2.1.0 build
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,3 +1,13 @@
|
|||||||
|
#### 2.1.0
|
||||||
|
- Added `XML` method
|
||||||
|
- `-q` option for quiet mode
|
||||||
|
- New wordlists backed by research
|
||||||
|
- `-oT` option for txt export
|
||||||
|
- `-oB` option for BurpSuite export
|
||||||
|
- `-oJ` alias for JSON export
|
||||||
|
- Added support for custom injection point in `XML` and `JSON`
|
||||||
|
- pypi package
|
||||||
|
|
||||||
#### 2.0-beta
|
#### 2.0-beta
|
||||||
- Added an anamoly detection algorithm with 9 factors
|
- Added an anamoly detection algorithm with 9 factors
|
||||||
- Added a HTTP response analyzer for handling errors and retrying requests
|
- Added a HTTP response analyzer for handling errors and retrying requests
|
||||||
|
|||||||
1
arjun/__init__.py
Normal file
1
arjun/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__version__ = '2.0.1'
|
||||||
@@ -1,15 +1,49 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
from __future__ import print_function
|
from arjun.core.colors import green, end, info, bad, good, run, res
|
||||||
|
|
||||||
from core.colors import green, end, info, bad, good, run, res
|
import os
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
import arjun.core.config as mem
|
||||||
|
from arjun.core.bruter import bruter
|
||||||
|
from arjun.core.exporter import exporter
|
||||||
|
from arjun.core.requester import requester
|
||||||
|
from arjun.core.anomaly import define
|
||||||
|
from arjun.core.utils import fetch_params, stable_request, random_str, slicer, confirm, populate, reader, nullify, prepare_requests
|
||||||
|
|
||||||
|
from arjun.plugins.heuristic import heuristic
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser() # defines the parser
|
||||||
|
# Arguments that can be supplied
|
||||||
|
parser.add_argument('-u', help='target url', dest='url')
|
||||||
|
parser.add_argument('-o', '-oJ', help='path for json output file', dest='json_file')
|
||||||
|
parser.add_argument('-oT', help='path for text output file', dest='text_file')
|
||||||
|
parser.add_argument('-oB', help='port for burp suite proxy', dest='burp_port')
|
||||||
|
parser.add_argument('-d', help='delay between requests', dest='delay', type=float, default=0)
|
||||||
|
parser.add_argument('-t', help='number of threads', dest='threads', type=int, default=2)
|
||||||
|
parser.add_argument('-w', help='wordlist path', dest='wordlist', default=os.getcwd()+'/db/default.txt')
|
||||||
|
parser.add_argument('-m', help='request method: GET/POST/XML/JSON', dest='method', default='GET')
|
||||||
|
parser.add_argument('-i', help='import targets from file', dest='import_file', nargs='?', const=True)
|
||||||
|
parser.add_argument('-T', help='http request timeout', dest='timeout', type=float, default=15)
|
||||||
|
parser.add_argument('-c', help='chunk size/number of parameters to be sent at once', type=int, dest='chunks', default=500)
|
||||||
|
parser.add_argument('-q', help='quiet mode, no output', dest='quiet', action='store_true')
|
||||||
|
parser.add_argument('--headers', help='add headers', dest='headers', nargs='?', const=True)
|
||||||
|
parser.add_argument('--passive', help='collect parameter names from passive sources', dest='passive')
|
||||||
|
parser.add_argument('--stable', help='prefer stability over speed', dest='stable', action='store_true')
|
||||||
|
parser.add_argument('--include', help='include this data in every request', dest='include', default={})
|
||||||
|
args = parser.parse_args() # arguments to be parsed
|
||||||
|
|
||||||
|
if args.quiet:
|
||||||
|
print = nullify
|
||||||
|
|
||||||
print('''%s _
|
print('''%s _
|
||||||
/_| _ '
|
/_| _ '
|
||||||
( |/ /(//) v2.0-beta
|
( |/ /(//) v%s
|
||||||
_/ %s
|
_/ %s
|
||||||
''' % (green, end))
|
''' % (green, __import__('arjun').__version__, end))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
@@ -17,39 +51,6 @@ except ImportError:
|
|||||||
print('%s Please use Python > 3.2 to run Arjun.' % bad)
|
print('%s Please use Python > 3.2 to run Arjun.' % bad)
|
||||||
quit()
|
quit()
|
||||||
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
import core.config as mem
|
|
||||||
from core.bruter import bruter
|
|
||||||
from core.prompt import prompt
|
|
||||||
from core.importer import importer
|
|
||||||
from core.requester import requester
|
|
||||||
from core.anamoly import define
|
|
||||||
from core.utils import fetch_params, stable_request, randomString, slicer, confirm, getParams, populate, extractHeaders, reader
|
|
||||||
|
|
||||||
from plugins.heuristic import heuristic
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser() # defines the parser
|
|
||||||
# Arguments that can be supplied
|
|
||||||
parser.add_argument('-u', help='target url', dest='url')
|
|
||||||
parser.add_argument('-o', help='path for the output file', dest='output_file')
|
|
||||||
parser.add_argument('-d', help='delay between requests', dest='delay', type=float, default=0)
|
|
||||||
parser.add_argument('-t', help='number of threads', dest='threads', type=int, default=2)
|
|
||||||
parser.add_argument('-w', help='wordlist path', dest='wordlist', default=sys.path[0]+'/db/params.txt')
|
|
||||||
parser.add_argument('-m', help='request method: GET/POST/JSON', dest='method', default='GET')
|
|
||||||
parser.add_argument('-i', help='import targets from file', dest='import_file', nargs='?', const=True)
|
|
||||||
parser.add_argument('-T', help='http request timeout', dest='timeout', type=float, default=15)
|
|
||||||
parser.add_argument('-c', help='chunk size/number of parameters to be sent at once', type=int, dest='chunks', default=500)
|
|
||||||
parser.add_argument('--headers', help='add headers', dest='headers', nargs='?', const=True)
|
|
||||||
parser.add_argument('--passive', help='collect parameter names from passive sources', dest='passive')
|
|
||||||
parser.add_argument('--stable', help='prefer stability over speed', dest='stable', action='store_true')
|
|
||||||
parser.add_argument('--include', help='include this data in every request', dest='include', default={})
|
|
||||||
args = parser.parse_args() # arguments to be parsed
|
|
||||||
|
|
||||||
mem.var = vars(args)
|
mem.var = vars(args)
|
||||||
|
|
||||||
mem.var['method'] = mem.var['method'].upper()
|
mem.var['method'] = mem.var['method'].upper()
|
||||||
@@ -58,7 +59,8 @@ if mem.var['stable'] or mem.var['delay']:
|
|||||||
mem.var['threads'] = 1
|
mem.var['threads'] = 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
wordlist = set(reader(args.wordlist, mode='lines'))
|
wordlist_file = os.getcwd() + '/db/small.txt' if args.wordlist == 'small' else args.wordlist
|
||||||
|
wordlist = set(reader(wordlist_file, mode='lines'))
|
||||||
if mem.var['passive']:
|
if mem.var['passive']:
|
||||||
host = mem.var['passive']
|
host = mem.var['passive']
|
||||||
if host == '-':
|
if host == '-':
|
||||||
@@ -72,40 +74,17 @@ except FileNotFoundError:
|
|||||||
exit('%s The specified file for parameters doesn\'t exist' % bad)
|
exit('%s The specified file for parameters doesn\'t exist' % bad)
|
||||||
|
|
||||||
if len(wordlist) < mem.var['chunks']:
|
if len(wordlist) < mem.var['chunks']:
|
||||||
mem.var['chunks'] = int(len(wordlist)/2)
|
mem.var['chunks'] = int(len(wordlist)/2)
|
||||||
|
|
||||||
if not (args.url, args.import_file):
|
if not args.url and not args.import_file:
|
||||||
exit('%s No targets specified' % bad)
|
exit('%s No target(s) specified' % bad)
|
||||||
|
|
||||||
def prepare_requests(args):
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0',
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
||||||
'Accept-Language': 'en-US,en;q=0.5',
|
|
||||||
'Accept-Encoding': 'gzip, deflate',
|
|
||||||
'Connection': 'close',
|
|
||||||
'Upgrade-Insecure-Requests': '1'
|
|
||||||
}
|
|
||||||
if type(headers) == bool:
|
|
||||||
headers = extractHeaders(prompt())
|
|
||||||
elif type(headers) == str:
|
|
||||||
headers = extractHeaders(headers)
|
|
||||||
if mem.var['method'] == 'JSON':
|
|
||||||
headers['Content-type'] = 'application/json'
|
|
||||||
if args.url:
|
|
||||||
params = getParams(args.include)
|
|
||||||
return {
|
|
||||||
'url': args.url,
|
|
||||||
'method': mem.var['method'],
|
|
||||||
'headers': headers,
|
|
||||||
'include': params
|
|
||||||
}
|
|
||||||
elif args.import_file:
|
|
||||||
return importer(args.import_file, mem.var['method'], headers, args.include)
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def narrower(request, factors, param_groups):
|
def narrower(request, factors, param_groups):
|
||||||
|
"""
|
||||||
|
takes a list of parameters and narrows it down to parameters that cause anomalies
|
||||||
|
returns list
|
||||||
|
"""
|
||||||
anamolous_params = []
|
anamolous_params = []
|
||||||
threadpool = ThreadPoolExecutor(max_workers=mem.var['threads'])
|
threadpool = ThreadPoolExecutor(max_workers=mem.var['threads'])
|
||||||
futures = (threadpool.submit(bruter, request, factors, params) for params in param_groups)
|
futures = (threadpool.submit(bruter, request, factors, params) for params in param_groups)
|
||||||
@@ -116,7 +95,12 @@ def narrower(request, factors, param_groups):
|
|||||||
print('%s Processing chunks: %i/%-6i' % (info, i + 1, len(param_groups)), end='\r')
|
print('%s Processing chunks: %i/%-6i' % (info, i + 1, len(param_groups)), end='\r')
|
||||||
return anamolous_params
|
return anamolous_params
|
||||||
|
|
||||||
|
|
||||||
def initialize(request, wordlist):
|
def initialize(request, wordlist):
|
||||||
|
"""
|
||||||
|
handles parameter finding process for a single request object
|
||||||
|
returns 'skipped' (on error), list on success
|
||||||
|
"""
|
||||||
url = request['url']
|
url = request['url']
|
||||||
if not url.startswith('http'):
|
if not url.startswith('http'):
|
||||||
print('%s %s is not a valid URL' % (bad, url))
|
print('%s %s is not a valid URL' % (bad, url))
|
||||||
@@ -126,11 +110,11 @@ def initialize(request, wordlist):
|
|||||||
if not stable:
|
if not stable:
|
||||||
return 'skipped'
|
return 'skipped'
|
||||||
else:
|
else:
|
||||||
fuzz = randomString(6)
|
fuzz = random_str(6)
|
||||||
response_1 = requester(request, {fuzz : fuzz[::-1]})
|
response_1 = requester(request, {fuzz: fuzz[::-1]})
|
||||||
print('%s Analysing HTTP response for anamolies' % run)
|
print('%s Analysing HTTP response for anamolies' % run)
|
||||||
fuzz = randomString(6)
|
fuzz = random_str(6)
|
||||||
response_2 = requester(request, {fuzz : fuzz[::-1]})
|
response_2 = requester(request, {fuzz: fuzz[::-1]})
|
||||||
if type(response_1) == str or type(response_2) == str:
|
if type(response_1) == str or type(response_2) == str:
|
||||||
return 'skipped'
|
return 'skipped'
|
||||||
factors = define(response_1, response_2, fuzz, fuzz[::-1], wordlist)
|
factors = define(response_1, response_2, fuzz, fuzz[::-1], wordlist)
|
||||||
@@ -157,42 +141,48 @@ def initialize(request, wordlist):
|
|||||||
if reason:
|
if reason:
|
||||||
name = list(param.keys())[0]
|
name = list(param.keys())[0]
|
||||||
confirmed_params.append(name)
|
confirmed_params.append(name)
|
||||||
print('%s name: %s, factor: %s' % (res, name, reason))
|
print('%s name: %s, factor: 4%s' % (res, name, reason))
|
||||||
return confirmed_params
|
return confirmed_params
|
||||||
|
|
||||||
request = prepare_requests(args)
|
|
||||||
|
|
||||||
final_result = {}
|
def main():
|
||||||
|
request = prepare_requests(args)
|
||||||
|
|
||||||
try:
|
final_result = {}
|
||||||
if type(request) == dict:
|
|
||||||
mem.var['kill'] = False
|
try:
|
||||||
url = request['url']
|
if type(request) == dict:
|
||||||
these_params = initialize(request, wordlist)
|
# in case of a single target
|
||||||
if these_params == 'skipped':
|
|
||||||
print('%s Skipped %s due to errors' % (bad, request['url']))
|
|
||||||
elif these_params:
|
|
||||||
final_result['url'] = url
|
|
||||||
final_result['params'] = these_params
|
|
||||||
final_result['method'] = request['method']
|
|
||||||
elif type(request) == list:
|
|
||||||
for each in request:
|
|
||||||
url = each['url']
|
|
||||||
mem.var['kill'] = False
|
mem.var['kill'] = False
|
||||||
print('%s Scanning: %s' % (run, url))
|
url = request['url']
|
||||||
these_params = initialize(each, list(wordlist))
|
these_params = initialize(request, wordlist)
|
||||||
if these_params == 'skipped':
|
if these_params == 'skipped':
|
||||||
print('%s Skipped %s due to errors' % (bad, url))
|
print('%s Skipped %s due to errors' % (bad, request['url']))
|
||||||
elif these_params:
|
elif these_ppiparams:
|
||||||
final_result[url] = {}
|
final_result[url] = {}
|
||||||
final_result[url]['params'] = these_params
|
final_result[url]['params'] = these_params
|
||||||
final_result[url]['method'] = each['method']
|
final_result[url]['method'] = request['method']
|
||||||
print('%s Parameters found: %s' % (good, ', '.join(final_result[url])))
|
final_result[url]['headers'] = request['headers']
|
||||||
except KeyboardInterrupt:
|
elif type(request) == list:
|
||||||
exit()
|
# in case of multiple targets
|
||||||
|
for each in request:
|
||||||
|
url = each['url']
|
||||||
|
mem.var['kill'] = False
|
||||||
|
print('%s Scanning: %s' % (run, url))
|
||||||
|
these_params = initialize(each, list(wordlist))
|
||||||
|
if these_params == 'skipped':
|
||||||
|
print('%s Skipped %s due to errors' % (bad, url))
|
||||||
|
elif these_params:
|
||||||
|
final_result[url] = {}
|
||||||
|
final_result[url]['params'] = these_params
|
||||||
|
final_result[url]['method'] = each['method']
|
||||||
|
final_result[url]['headers'] = each['headers']
|
||||||
|
print('%s Parameters found: %s' % (good, ', '.join(final_result[url])))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
exit()
|
||||||
|
|
||||||
# Finally, export to json
|
exporter(final_result)
|
||||||
if args.output_file and final_result:
|
|
||||||
with open(str(mem.var['output_file']), 'w+', encoding='utf8') as json_output:
|
|
||||||
json.dump(final_result, json_output, sort_keys=True, indent=4)
|
if __name__ == '__main__':
|
||||||
print('%s Output saved to JSON file in %s' % (info, mem.var['output_file']))
|
main()
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from core.utils import lcs, removeTags
|
from arjun.core.utils import lcs, diff_map, remove_tags
|
||||||
|
|
||||||
def diff_map(body_1, body_2):
|
|
||||||
sig = []
|
|
||||||
lines_1, lines_2 = body_1.split('\n'), body_2.split('\n')
|
|
||||||
for line_1, line_2 in zip(lines_1, lines_2):
|
|
||||||
if line_1 == line_2:
|
|
||||||
sig.append(line_1)
|
|
||||||
return sig
|
|
||||||
|
|
||||||
|
|
||||||
def define(response_1, response_2, param, value, wordlist):
|
def define(response_1, response_2, param, value, wordlist):
|
||||||
|
"""
|
||||||
|
defines a rule list for detecting anomalies by comparing two HTTP response
|
||||||
|
returns dict
|
||||||
|
"""
|
||||||
factors = {
|
factors = {
|
||||||
'same_code': False, # if http status code is same, contains that code
|
'same_code': False, # if http status code is same, contains that code
|
||||||
'same_body': False, # if http body is same, contains that body
|
'same_body': False, # if http body is same, contains that body
|
||||||
@@ -23,7 +19,7 @@ def define(response_1, response_2, param, value, wordlist):
|
|||||||
'param_missing': False, # if param name is missing from the body, contains words that are already there
|
'param_missing': False, # if param name is missing from the body, contains words that are already there
|
||||||
'value_missing': False # contains whether param value is missing from the body
|
'value_missing': False # contains whether param value is missing from the body
|
||||||
}
|
}
|
||||||
if (response_1 and response_2) != None:
|
if response_1 and response_2:
|
||||||
body_1, body_2 = response_1.text, response_2.text
|
body_1, body_2 = response_1.text, response_2.text
|
||||||
if response_1.status_code == response_2.status_code:
|
if response_1.status_code == response_2.status_code:
|
||||||
factors['same_code'] = response_1.status_code
|
factors['same_code'] = response_1.status_code
|
||||||
@@ -33,8 +29,8 @@ def define(response_1, response_2, param, value, wordlist):
|
|||||||
factors['same_redirect'] = response_1.url
|
factors['same_redirect'] = response_1.url
|
||||||
if response_1.text == response_2.text:
|
if response_1.text == response_2.text:
|
||||||
factors['same_body'] = response_1.text
|
factors['same_body'] = response_1.text
|
||||||
elif removeTags(body_1) == removeTags(body_2):
|
elif remove_tags(body_1) == remove_tags(body_2):
|
||||||
factors['same_plaintext'] = removeTags(body_1)
|
factors['same_plaintext'] = remove_tags(body_1)
|
||||||
elif body_1 and body_2:
|
elif body_1 and body_2:
|
||||||
if body_1.count('\\n') == 1:
|
if body_1.count('\\n') == 1:
|
||||||
factors['common_string'] = lcs(body_1, body_2)
|
factors['common_string'] = lcs(body_1, body_2)
|
||||||
@@ -48,6 +44,10 @@ def define(response_1, response_2, param, value, wordlist):
|
|||||||
|
|
||||||
|
|
||||||
def compare(response, factors, params):
|
def compare(response, factors, params):
|
||||||
|
"""
|
||||||
|
detects anomalies by comparing a HTTP response against a rule list
|
||||||
|
returns string, list (anamoly, list of parameters that caused it)
|
||||||
|
"""
|
||||||
if factors['same_code'] and response.status_code != factors['same_code']:
|
if factors['same_code'] and response.status_code != factors['same_code']:
|
||||||
return ('http code', params)
|
return ('http code', params)
|
||||||
if factors['same_headers'] and list(response.headers.keys()) != factors['same_headers']:
|
if factors['same_headers'] and list(response.headers.keys()) != factors['same_headers']:
|
||||||
@@ -56,7 +56,7 @@ def compare(response, factors, params):
|
|||||||
return ('redirection', params)
|
return ('redirection', params)
|
||||||
if factors['same_body'] and response.text != factors['same_body']:
|
if factors['same_body'] and response.text != factors['same_body']:
|
||||||
return ('body length', params)
|
return ('body length', params)
|
||||||
if factors['same_plaintext'] and removeTags(response.text) != factors['same_plaintext']:
|
if factors['same_plaintext'] and remove_tags(response.text) != factors['same_plaintext']:
|
||||||
return ('text length', params)
|
return ('text length', params)
|
||||||
if factors['lines_diff']:
|
if factors['lines_diff']:
|
||||||
for line in factors['lines_diff']:
|
for line in factors['lines_diff']:
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import core.config as mem
|
import arjun.core.config as mem
|
||||||
|
|
||||||
from core.anamoly import compare
|
from arjun.core.anomaly import compare
|
||||||
from core.requester import requester
|
from arjun.core.requester import requester
|
||||||
from core.error_handler import error_handler
|
from arjun.core.error_handler import error_handler
|
||||||
|
|
||||||
|
|
||||||
def bruter(request, factors, params, mode='bruteforce'):
|
def bruter(request, factors, params, mode='bruteforce'):
|
||||||
|
"""
|
||||||
|
returns anomaly detection result for a chunk of parameters
|
||||||
|
returns list
|
||||||
|
"""
|
||||||
if mem.var['kill']:
|
if mem.var['kill']:
|
||||||
return []
|
return []
|
||||||
response = requester(request, params)
|
response = requester(request, params)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
var = {}
|
var = {} # all the cli arguments are added to this variable to be accessed globally
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import core.config as mem
|
import arjun.core.config as mem
|
||||||
|
|
||||||
from core.colors import bad
|
from arjun.core.colors import bad
|
||||||
|
|
||||||
def connection_refused():
|
def connection_refused():
|
||||||
|
"""
|
||||||
|
checks if a request should be retried if the server refused connection
|
||||||
|
returns str
|
||||||
|
"""
|
||||||
if mem.var['stable']:
|
if mem.var['stable']:
|
||||||
print('%s Hit rate limit, stabilizing the connection' % bad)
|
print('%s Hit rate limit, stabilizing the connection' % bad)
|
||||||
mem.var['kill'] = False
|
mem.var['kill'] = False
|
||||||
time.sleep(30)
|
time.sleep(30)
|
||||||
return 'retry'
|
return 'retry'
|
||||||
@@ -14,11 +18,18 @@ def connection_refused():
|
|||||||
return 'kill'
|
return 'kill'
|
||||||
|
|
||||||
def error_handler(response, factors):
|
def error_handler(response, factors):
|
||||||
|
"""
|
||||||
|
decides what to do after performing a HTTP request
|
||||||
|
'ok': continue normally
|
||||||
|
'retry': retry this request
|
||||||
|
'kill': stop processing this target
|
||||||
|
returns str
|
||||||
|
"""
|
||||||
if type(response) != str and response.status_code in (400, 503, 429):
|
if type(response) != str and response.status_code in (400, 503, 429):
|
||||||
if response.status_code == 400:
|
if response.status_code == 400:
|
||||||
if factors['same_code'] != 400:
|
if factors['same_code'] != 400:
|
||||||
mem.var['kill'] = True
|
mem.var['kill'] = True
|
||||||
print('%s Server recieved a bad request. Try decreasing the chunk size with -c option' % bad)
|
print('%s Server recieved a bad request. Try decreasing the chunk size with -c option' % bad)
|
||||||
return 'kill'
|
return 'kill'
|
||||||
else:
|
else:
|
||||||
return 'ok'
|
return 'ok'
|
||||||
@@ -35,7 +46,7 @@ def error_handler(response, factors):
|
|||||||
print('%s Connection timed out, unable to increase timeout further')
|
print('%s Connection timed out, unable to increase timeout further')
|
||||||
return 'kill'
|
return 'kill'
|
||||||
else:
|
else:
|
||||||
print('%s Connection timed out, increased timeout by 5 seconds' % bad)
|
print('%s Connection timed out, increased timeout by 5 seconds' % bad)
|
||||||
mem.var['timeout'] += 5
|
mem.var['timeout'] += 5
|
||||||
return 'retry'
|
return 'retry'
|
||||||
elif 'ConnectionRefused' in response:
|
elif 'ConnectionRefused' in response:
|
||||||
|
|||||||
57
arjun/core/exporter.py
Normal file
57
arjun/core/exporter.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
import requests
|
||||||
|
|
||||||
|
import arjun.core.config as mem
|
||||||
|
from arjun.core.utils import populate
|
||||||
|
|
||||||
|
from arjun.core.utils import create_query_string
|
||||||
|
|
||||||
|
def json_export(result):
|
||||||
|
"""
|
||||||
|
exports result to a file in JSON format
|
||||||
|
"""
|
||||||
|
with open(mem.var['json_file'], 'w+', encoding='utf8') as json_output:
|
||||||
|
json.dump(result, json_output, sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
def burp_export(result):
|
||||||
|
"""
|
||||||
|
exports results to Burp Suite by sending request to Burp proxy
|
||||||
|
"""
|
||||||
|
for url, data in result.items():
|
||||||
|
url = re.sub(r'://[^/]+', '://' + mem.var['burp_port'], url, 1)
|
||||||
|
if data['method'] == 'GET':
|
||||||
|
requests.get(url, params=populate(data['params']), headers=data['headers'])
|
||||||
|
elif data['method'] == 'POST':
|
||||||
|
requests.post(url, data=populate(data['params']), headers=data['headers'])
|
||||||
|
elif data['method'] == 'JSON':
|
||||||
|
requests.post(url, json=populate(data['params']), headers=data['headers'])
|
||||||
|
|
||||||
|
def text_export(result):
|
||||||
|
"""
|
||||||
|
exports results to a text file, one url per line
|
||||||
|
"""
|
||||||
|
with open(mem.var['text_file'], 'w+', encoding='utf8') as text_file:
|
||||||
|
for url, data in result.items():
|
||||||
|
clean_url = url.lstrip('/')
|
||||||
|
if data['method'] == 'JSON':
|
||||||
|
text_file.write(clean_url + '\t' + json.dumps(populate(data['params'])) + '\n')
|
||||||
|
else:
|
||||||
|
query_string = create_query_string(data['params'])
|
||||||
|
if '?' in clean_url:
|
||||||
|
query_string = query_string.replace('?', '&', 1)
|
||||||
|
if data['method'] == 'GET':
|
||||||
|
text_file.write(clean_url + query_string + '\n')
|
||||||
|
elif data['method'] == 'POST':
|
||||||
|
text_file.write(clean_url + '\t' + query_string + '\n')
|
||||||
|
|
||||||
|
def exporter(result):
|
||||||
|
"""
|
||||||
|
main exporter function that calls other export functions
|
||||||
|
"""
|
||||||
|
if mem.var['json_file']:
|
||||||
|
json_export(result)
|
||||||
|
if mem.var['text_file']:
|
||||||
|
text_export(result)
|
||||||
|
if mem.var['burp_port']:
|
||||||
|
burp_export(result)
|
||||||
@@ -1,5 +1,28 @@
|
|||||||
import re
|
import re
|
||||||
from core.utils import reader, parse_request
|
|
||||||
|
def reader(path, mode='string'):
|
||||||
|
"""
|
||||||
|
reads a file
|
||||||
|
returns a string/array containing the content of the file
|
||||||
|
"""
|
||||||
|
with open(path, 'r', encoding='utf-8') as file:
|
||||||
|
if mode == 'lines':
|
||||||
|
return list(filter(None, [line.rstrip('\n') for line in file]))
|
||||||
|
else:
|
||||||
|
return ''.join([line for line in file])
|
||||||
|
|
||||||
|
def parse_request(string):
|
||||||
|
"""
|
||||||
|
parses http request
|
||||||
|
returns dict
|
||||||
|
"""
|
||||||
|
result = {}
|
||||||
|
match = re.search(r'(?:([a-zA-Z0-9]+) ([^ ]+) [^ ]+\n)?([\s\S]+\n)\n?([\s\S]+)?', string)
|
||||||
|
result['method'] = match.group(1)
|
||||||
|
result['path'] = match.group(2)
|
||||||
|
result['headers'] = parse_headers(match.group(3))
|
||||||
|
result['data'] = match.group(4)
|
||||||
|
return result
|
||||||
|
|
||||||
burp_regex = re.compile(r'''(?m)^ <url><!\[CDATA\[(.+?)\]\]></url>
|
burp_regex = re.compile(r'''(?m)^ <url><!\[CDATA\[(.+?)\]\]></url>
|
||||||
<host ip="[^"]*">[^<]+</host>
|
<host ip="[^"]*">[^<]+</host>
|
||||||
@@ -15,44 +38,59 @@ burp_regex = re.compile(r'''(?m)^ <url><!\[CDATA\[(.+?)\]\]></url>
|
|||||||
|
|
||||||
|
|
||||||
def burp_import(path):
|
def burp_import(path):
|
||||||
requests = []
|
"""
|
||||||
content = reader(path)
|
imports targets from burp suite
|
||||||
matches = re.finditer(burp_regex, content)
|
returns list (of request objects)
|
||||||
for match in matches:
|
"""
|
||||||
request = parse_request(match.group(4))
|
requests = []
|
||||||
headers = request['headers']
|
content = reader(path)
|
||||||
if match.group(7) in ('HTML', 'JSON'):
|
matches = re.finditer(burp_regex, content)
|
||||||
requests.append({
|
for match in matches:
|
||||||
'url': match.group(1),
|
request = parse_request(match.group(4))
|
||||||
'method': match.group(2),
|
headers = request['headers']
|
||||||
'extension': match.group(3),
|
if match.group(7) in ('HTML', 'JSON'):
|
||||||
'headers': headers,
|
requests.append({
|
||||||
'include': request['data'],
|
'url': match.group(1),
|
||||||
'code': match.group(5),
|
'method': match.group(2),
|
||||||
'length': match.group(6),
|
'extension': match.group(3),
|
||||||
'mime': match.group(7)
|
'headers': headers,
|
||||||
})
|
'include': request['data'],
|
||||||
return requests
|
'code': match.group(5),
|
||||||
|
'length': match.group(6),
|
||||||
|
'mime': match.group(7)
|
||||||
|
})
|
||||||
|
return requests
|
||||||
|
|
||||||
|
|
||||||
def urls_import(path, method, headers, include):
|
def urls_import(path, method, headers, include):
|
||||||
requests = []
|
"""
|
||||||
urls = reader(path, mode='lines')
|
imports urls from a newline delimited text file
|
||||||
for url in urls:
|
returns list (of request objects)
|
||||||
requests.append({
|
"""
|
||||||
'url': url,
|
requests = []
|
||||||
'method': method,
|
urls = reader(path, mode='lines')
|
||||||
'headers': headers,
|
for url in urls:
|
||||||
'data': include
|
requests.append({
|
||||||
})
|
'url': url,
|
||||||
return requests
|
'method': method,
|
||||||
|
'headers': headers,
|
||||||
|
'data': include
|
||||||
|
})
|
||||||
|
return requests
|
||||||
|
|
||||||
|
|
||||||
def request_import(path):
|
def request_import(path):
|
||||||
return parse_request(reader(path))
|
"""
|
||||||
|
imports request from a raw request file
|
||||||
|
returns dict
|
||||||
|
"""
|
||||||
|
return parse_request(reader(path))
|
||||||
|
|
||||||
|
|
||||||
def importer(path, method, headers, include):
|
def importer(path, method, headers, include):
|
||||||
|
"""
|
||||||
|
main importer function that calls other import functions
|
||||||
|
"""
|
||||||
with open(path, 'r', encoding='utf-8') as file:
|
with open(path, 'r', encoding='utf-8') as file:
|
||||||
for line in file:
|
for line in file:
|
||||||
if line.startswith('<?xml'):
|
if line.startswith('<?xml'):
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import os
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
def prompt(default=None):
|
def prompt(default=None):
|
||||||
|
"""
|
||||||
|
lets user paste input by opening a temp file in a text editor
|
||||||
|
returns str (content of tmp file)
|
||||||
|
"""
|
||||||
editor = 'nano'
|
editor = 'nano'
|
||||||
with tempfile.NamedTemporaryFile(mode='r+') as tmpfile:
|
with tempfile.NamedTemporaryFile(mode='r+') as tmpfile:
|
||||||
if default:
|
if default:
|
||||||
@@ -16,4 +20,4 @@ def prompt(default=None):
|
|||||||
else:
|
else:
|
||||||
os.waitpid(child_pid, 0)
|
os.waitpid(child_pid, 0)
|
||||||
tmpfile.seek(0)
|
tmpfile.seek(0)
|
||||||
return tmpfile.read().strip()
|
return tmpfile.read().strip()
|
||||||
|
|||||||
@@ -1,31 +1,40 @@
|
|||||||
import re
|
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import random
|
import random
|
||||||
import requests
|
import requests
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import core.config as mem
|
import arjun.core.config as mem
|
||||||
|
|
||||||
|
from arjun.core.utils import dict_to_xml
|
||||||
|
|
||||||
warnings.filterwarnings('ignore') # Disable SSL related warnings
|
warnings.filterwarnings('ignore') # Disable SSL related warnings
|
||||||
|
|
||||||
def requester(request, payload={}):
|
def requester(request, payload={}):
|
||||||
|
"""
|
||||||
|
central function for making http requests
|
||||||
|
returns str on error otherwise response object of requests library
|
||||||
|
"""
|
||||||
if 'include' in request and request['include']:
|
if 'include' in request and request['include']:
|
||||||
payload.update(request['include'])
|
payload.update(request['include'])
|
||||||
if mem.var['stable']:
|
if mem.var['stable']:
|
||||||
mem.var['delay'] = random.choice(range(6, 12))
|
mem.var['delay'] = random.choice(range(6, 12))
|
||||||
time.sleep(mem.var['delay'])
|
time.sleep(mem.var['delay'])
|
||||||
url = request['url']
|
url = request['url']
|
||||||
if 'Host' not in request['headers']:
|
|
||||||
this_host = re.search(r'https?://([^/]+)', url).group(1)
|
|
||||||
request['headers']['Host'] = this_host.split('@')[1] if '@' in this_host else this_host
|
|
||||||
if mem.var['kill']:
|
if mem.var['kill']:
|
||||||
return 'killed'
|
return 'killed'
|
||||||
try:
|
try:
|
||||||
if request['method'] == 'GET':
|
if request['method'] == 'GET':
|
||||||
response = requests.get(url, params=payload, headers=request['headers'], verify=False, timeout=mem.var['timeout'])
|
response = requests.get(url, params=payload, headers=request['headers'], verify=False, timeout=mem.var['timeout'])
|
||||||
elif request['method'] == 'JSON':
|
elif request['method'] == 'JSON':
|
||||||
response = requests.post(url, json=json.dumps(payload), headers=request['headers'], verify=False, timeout=mem.var['timeout'])
|
if mem.var['include'] and '$arjun$' in mem.var['include']:
|
||||||
|
payload = mem.var['include'].replace('$arjun$', json.dumps(payload).rstrip('}').lstrip('{'))
|
||||||
|
response = requests.post(url, data=payload, headers=request['headers'], verify=False, timeout=mem.var['timeout'])
|
||||||
|
else:
|
||||||
|
response = requests.post(url, json=payload, headers=request['headers'], verify=False, timeout=mem.var['timeout'])
|
||||||
|
elif request['method'] == 'XML':
|
||||||
|
payload = mem.var['include'].replace('$arjun$', dict_to_xml(payload))
|
||||||
|
response = requests.post(url, data=payload, headers=request['headers'], verify=False, timeout=mem.var['timeout'])
|
||||||
else:
|
else:
|
||||||
response = requests.post(url, data=payload, headers=request['headers'], verify=False, timeout=mem.var['timeout'])
|
response = requests.post(url, data=payload, headers=request['headers'], verify=False, timeout=mem.var['timeout'])
|
||||||
return response
|
return response
|
||||||
|
|||||||
@@ -4,13 +4,19 @@ import random
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
|
from dicttoxml import dicttoxml
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from plugins.otx import otx
|
from arjun.core.prompt import prompt
|
||||||
from plugins.wayback import wayback
|
from arjun.core.importer import importer
|
||||||
from plugins.commoncrawl import commoncrawl
|
|
||||||
|
from arjun.plugins.otx import otx
|
||||||
|
from arjun.plugins.wayback import wayback
|
||||||
|
from arjun.plugins.commoncrawl import commoncrawl
|
||||||
|
|
||||||
|
import arjun.core.config as mem
|
||||||
|
from arjun.core.colors import info
|
||||||
|
|
||||||
from core.colors import info
|
|
||||||
|
|
||||||
def lcs(s1, s2):
|
def lcs(s1, s2):
|
||||||
"""
|
"""
|
||||||
@@ -31,7 +37,7 @@ def lcs(s1, s2):
|
|||||||
return s1[x_longest - longest: x_longest]
|
return s1[x_longest - longest: x_longest]
|
||||||
|
|
||||||
|
|
||||||
def extractHeaders(headers):
|
def extract_headers(headers):
|
||||||
"""
|
"""
|
||||||
parses headers provided through command line
|
parses headers provided through command line
|
||||||
returns dict
|
returns dict
|
||||||
@@ -93,44 +99,45 @@ def stable_request(url, headers):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def removeTags(html):
|
def remove_tags(html):
|
||||||
"""
|
"""
|
||||||
removes all the html from a webpage source
|
removes all the html from a webpage source
|
||||||
"""
|
"""
|
||||||
return re.sub(r'(?s)<.*?>', '', html)
|
return re.sub(r'(?s)<.*?>', '', html)
|
||||||
|
|
||||||
|
|
||||||
def lineComparer(response1, response2):
|
def diff_map(body_1, body_2):
|
||||||
"""
|
"""
|
||||||
compares two webpage and finds the non-matching lines
|
creates a list of lines that are common between two multi-line strings
|
||||||
|
returns list
|
||||||
"""
|
"""
|
||||||
response1 = response1.split('\n')
|
sig = []
|
||||||
response2 = response2.split('\n')
|
lines_1, lines_2 = body_1.split('\n'), body_2.split('\n')
|
||||||
num = 0
|
for line_1, line_2 in zip(lines_1, lines_2):
|
||||||
dynamicLines = []
|
if line_1 == line_2:
|
||||||
for line1, line2 in zip(response1, response2):
|
sig.append(line_1)
|
||||||
if line1 != line2:
|
return sig
|
||||||
dynamicLines.append(num)
|
|
||||||
num += 1
|
|
||||||
return dynamicLines
|
|
||||||
|
|
||||||
|
|
||||||
def randomString(n):
|
def random_str(n):
|
||||||
"""
|
"""
|
||||||
generates a random string of length n
|
generates a random string of length n
|
||||||
"""
|
"""
|
||||||
return ''.join(str(random.choice(range(10))) for i in range(n))
|
return ''.join(str(random.choice(range(10))) for i in range(n))
|
||||||
|
|
||||||
|
|
||||||
def getParams(include):
|
def get_params(include):
|
||||||
"""
|
"""
|
||||||
loads parameters from JSON/query string
|
loads parameters from JSON/query string
|
||||||
"""
|
"""
|
||||||
params = {}
|
params = {}
|
||||||
if include:
|
if include:
|
||||||
if include.startswith('{'):
|
if include.startswith('{'):
|
||||||
params = json.loads(str(include).replace('\'', '"'))
|
try:
|
||||||
return params
|
params = json.loads(str(include).replace('\'', '"'))
|
||||||
|
return params
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
return {}
|
||||||
else:
|
else:
|
||||||
cleaned = include.split('?')[-1]
|
cleaned = include.split('?')[-1]
|
||||||
parts = cleaned.split('&')
|
parts = cleaned.split('&')
|
||||||
@@ -143,6 +150,18 @@ def getParams(include):
|
|||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def create_query_string(params):
|
||||||
|
"""
|
||||||
|
creates a query string from a list of parameters
|
||||||
|
returns str
|
||||||
|
"""
|
||||||
|
query_string = ''
|
||||||
|
for param in params:
|
||||||
|
pair = param + '=' + random_str(4)
|
||||||
|
query_string += pair
|
||||||
|
return '?' + query_string
|
||||||
|
|
||||||
|
|
||||||
def reader(path, mode='string'):
|
def reader(path, mode='string'):
|
||||||
"""
|
"""
|
||||||
reads a file
|
reads a file
|
||||||
@@ -225,3 +244,50 @@ def fetch_params(host):
|
|||||||
page += 1
|
page += 1
|
||||||
print('%s Progress: %i%%' % (info, 100), end='\r')
|
print('%s Progress: %i%%' % (info, 100), end='\r')
|
||||||
return params
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_requests(args):
|
||||||
|
"""
|
||||||
|
creates a list of request objects used by Arjun from targets given by user
|
||||||
|
returns list (of targs)
|
||||||
|
"""
|
||||||
|
headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'Connection': 'close',
|
||||||
|
'Upgrade-Insecure-Requests': '1'
|
||||||
|
}
|
||||||
|
if type(headers) == bool:
|
||||||
|
headers = extract_headers(prompt())
|
||||||
|
elif type(headers) == str:
|
||||||
|
headers = extract_headers(headers)
|
||||||
|
if mem.var['method'] == 'JSON':
|
||||||
|
headers['Content-type'] = 'application/json'
|
||||||
|
if args.url:
|
||||||
|
params = get_params(args.include)
|
||||||
|
return {
|
||||||
|
'url': args.url,
|
||||||
|
'method': mem.var['method'],
|
||||||
|
'headers': headers,
|
||||||
|
'include': params
|
||||||
|
}
|
||||||
|
elif args.import_file:
|
||||||
|
return importer(args.import_file, mem.var['method'], headers, args.include)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def nullify(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
a function that does nothing
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def dict_to_xml(dict_obj):
|
||||||
|
"""
|
||||||
|
converts dict to xml string
|
||||||
|
returns str
|
||||||
|
"""
|
||||||
|
return dicttoxml(dict_obj, root=False, attr_type=False).decode('utf-8')
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,26 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from core.utils import extract_js
|
from arjun.core.utils import extract_js
|
||||||
|
|
||||||
def is_not_junk(string):
|
def is_not_junk(string):
|
||||||
return re.match(r'^[A-Za-z0-9_]+$', string)
|
return re.match(r'^[A-Za-z0-9_]+$', string)
|
||||||
|
|
||||||
def heuristic(response, paramList):
|
def insert_words(words, wordlist, found):
|
||||||
|
if words:
|
||||||
|
for var in words:
|
||||||
|
if var not in found and is_not_junk(var):
|
||||||
|
found.append(var)
|
||||||
|
if var in wordlist:
|
||||||
|
wordlist.remove(var)
|
||||||
|
wordlist.insert(0, var)
|
||||||
|
|
||||||
|
def heuristic(response, wordlist):
|
||||||
found = []
|
found = []
|
||||||
inputs = re.findall(r'(?i)<input.+?name=["\']?([^"\'\s>]+)', response)
|
inputs = re.findall(r'(?i)<input.+?name=["\']?([^"\'\s>]+)', response)
|
||||||
if inputs:
|
insert_words(inputs, wordlist, found)
|
||||||
for inpName in inputs:
|
|
||||||
if inpName not in found and is_not_junk(inpName):
|
|
||||||
if inpName in paramList:
|
|
||||||
paramList.remove(inpName)
|
|
||||||
found.append(inpName)
|
|
||||||
paramList.insert(0, inpName)
|
|
||||||
for script in extract_js(response):
|
for script in extract_js(response):
|
||||||
emptyJSvars = re.findall(r'([^\s!=<>]+)\s*=\s*[\'"`][\'"`]', script)
|
empty_vars = re.findall(r'([^\s!=<>]+)\s*=\s*[\'"`][\'"`]', script)
|
||||||
if emptyJSvars:
|
insert_words(empty_vars, wordlist, found)
|
||||||
for var in emptyJSvars:
|
map_keys = re.findall(r'([^\'"]+)[\'"]:\s?[\'"]', script)
|
||||||
if var not in found and is_not_junk(var):
|
insert_words(map_keys, wordlist, found)
|
||||||
found.append(var)
|
|
||||||
if var in paramList:
|
|
||||||
paramList.remove(var)
|
|
||||||
paramList.insert(0, var)
|
|
||||||
arrayJSnames = re.findall(r'([^\'"]+)[\'"]:\s?[\'"]', script)
|
|
||||||
if arrayJSnames:
|
|
||||||
for var in arrayJSnames:
|
|
||||||
if var not in found and is_not_junk(var):
|
|
||||||
found.append(var)
|
|
||||||
if var in paramList:
|
|
||||||
paramList.remove(var)
|
|
||||||
paramList.insert(0, var)
|
|
||||||
return found
|
return found
|
||||||
|
|||||||
42
setup.py
Normal file
42
setup.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import io
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
from os import path
|
||||||
|
this_directory = path.abspath(path.dirname(__file__))
|
||||||
|
with io.open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
|
||||||
|
desc = f.read()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name='arjun',
|
||||||
|
version=__import__('arjun').__version__,
|
||||||
|
description='HTTP parameter discovery suite',
|
||||||
|
long_description=desc,
|
||||||
|
long_description_content_type='text/markdown',
|
||||||
|
author='Somdev Sangwan',
|
||||||
|
author_email='s0md3v@gmail.com',
|
||||||
|
license='GNU General Public License v3 (GPLv3)',
|
||||||
|
url='https://github.com/s0md3v/Arjun',
|
||||||
|
download_url='https://github.com/s0md3v/Arjun/archive/v%s.zip' % __import__('arjun').__version__,
|
||||||
|
packages=find_packages(),
|
||||||
|
install_requires=[
|
||||||
|
'requests',
|
||||||
|
'dicttoxml'
|
||||||
|
],
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 5 - Production/Stable',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'Intended Audience :: Information Technology',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
'Topic :: Security',
|
||||||
|
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||||
|
'Programming Language :: Python :: 3.4',
|
||||||
|
],
|
||||||
|
entry_points={
|
||||||
|
'console_scripts': [
|
||||||
|
'arjun = arjun.__main__:main'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
keywords=['arjun', 'bug bounty', 'http', 'pentesting', 'security'],
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user