2.1.0 build

This commit is contained in:
Somdev Sangwan
2021-02-07 19:43:30 +05:30
parent 03545a1ed8
commit aa12f1f16f
16 changed files with 431 additions and 210 deletions

View File

@@ -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
View File

@@ -0,0 +1 @@
__version__ = '2.0.1'

View File

@@ -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()

View File

@@ -1 +0,0 @@

View File

@@ -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']:

View File

@@ -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)

View File

@@ -1 +1 @@
var = {} var = {} # all the cli arguments are added to this variable to be accessed globally

View File

@@ -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
View 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)

View File

@@ -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'):

View File

@@ -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()

View File

@@ -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

View File

@@ -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')

View File

@@ -1 +0,0 @@

View File

@@ -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
View 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'],
)