Compare commits

..

10 Commits

Author SHA1 Message Date
Somdev Sangwan
d1fb995cb1 Merge pull request #225 from c4v4r0n/master
fix - bug if response_1 is a str
2025-02-20 16:22:16 +05:30
c4v4r0n
6f87dccd17 fix - bug if response_1 is a str 2025-02-06 20:47:01 -03:00
Somdev Sangwan
c7d97358b0 Merge pull request #219 from gildasio/master
Fix wrong attribute access
2024-12-17 15:38:32 +05:30
Gildasio Junior
38f307d92a Fix wrong attribute access
When response code is any of 400, 413, 418, 429, 503,
mem.var['healthy_url'] is set to False, which in turn triggers the print
on line 135. But there 'request.status_code' is accessed instead
'response_1.status_code'.

closes #218
2024-11-11 16:37:19 -03:00
Somdev Sangwan
1b251b023b pip3->pipx 2024-11-04 02:10:49 +05:30
Somdev Sangwan
1b11c3574e 2.2.7 build 2024-11-04 01:59:30 +05:30
Somdev Sangwan
93c6c966ee [REF] main: refactor the entry function to make it more readable 2024-11-03 15:26:10 +05:30
Somdev Sangwan
53afa55518 Merge pull request #212 from jojonas/empty_factors
Fix a bug with empty factors
2024-06-05 23:31:34 +05:30
Jonas Lieb
afc11b0769 Fix a bug with empty factors
If a factor has been defined but evaluates to "False" (e.g. an empty
body), this factor would be skipped during the compare phase. This
commit changes the behaviour to use "None" as a placeholder instead of
every False-ish value.
2024-04-25 09:42:50 +02:00
Ahmed Mohamed
c1fcde3ac5 [REF] main: refactor the entry function to make it more readable 2024-04-15 20:34:13 +02:00
16 changed files with 192 additions and 94 deletions

View File

@@ -1,3 +1,9 @@
#### 2.2.7
- Added `--casing` option for casing style enforcement
- Added `--ratelimit` option for explicitly defining requests/second
- Fixed "decrease chunk size/use --stable" type errors in some cases
- Fixed a bug in anamoly detection
#### 2.2.6
- Fixed Arjun getting infinitely stuck on some webpages

View File

@@ -48,17 +48,11 @@ The best part? It takes less than 10 seconds to go through this huge list while
- Can passively extract parameters from JS or 3 external sources
### Installing Arjun
You can install `arjun` with pip as following:
The recommended way to install `arjun` is as following:
```
pip3 install arjun
```
or, by downloading this repository and running
```
python3 setup.py install
pipx install arjun
```
> Note: If you are using an old version of python, use pip instead of pipx.
### How to use Arjun?

View File

@@ -1 +1 @@
__version__ = '2.2.6'
__version__ = '2.2.7'

View File

@@ -13,6 +13,7 @@ from arjun.core.anomaly import define, compare
from arjun.core.utils import fetch_params, stable_request, random_str, slicer, confirm, populate, reader, nullify, prepare_requests, compatible_path
from arjun.plugins.heuristic import heuristic
from arjun.plugins.wl import detect_casing, covert_to_case
arjun_dir = compatible_path(mem.__file__.replace(compatible_path('/core/config.py'), ''))
@@ -25,7 +26,7 @@ parser.add_argument('-oB', help='Output to Burp Suite Proxy. Default is 127.0.0.
parser.add_argument('-d', help='Delay between requests in seconds. (default: 0)', dest='delay', type=float, default=0)
parser.add_argument('-t', help='Number of concurrent threads. (default: 5)', dest='threads', type=int, default=5)
parser.add_argument('-w', help='Wordlist file path. (default: {arjundir}/db/large.txt)', dest='wordlist', default=arjun_dir+'/db/large.txt')
parser.add_argument('-m', help='Request method to use: GET/POST/XML/JSON/HEADERS. (default: GET)', dest='method', default='GET')
parser.add_argument('-m', help='Request method to use: GET/POST/XML/JSON. (default: GET)', dest='method', default='GET')
parser.add_argument('-i', help='Import target URLs from file.', dest='import_file', nargs='?', const=True)
parser.add_argument('-T', help='HTTP request timeout in seconds. (default: 15)', dest='timeout', type=float, default=15)
parser.add_argument('-c', help='Chunk size. The number of parameters to be sent at once', type=int, dest='chunks', default=250)
@@ -36,6 +37,7 @@ parser.add_argument('--passive', help='Collect parameter names from passive sour
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={})
parser.add_argument('--disable-redirects', help='disable redirects', dest='disable_redirects', action='store_true')
parser.add_argument('--casing', help='casing style for params e.g. like_this, likeThis, likethis', dest='casing')
args = parser.parse_args() # arguments to be parsed
if args.quiet:
@@ -77,7 +79,11 @@ try:
passive_params = fetch_params(host)
wordlist.update(passive_params)
print('%s Collected %s parameters, added to the wordlist' % (info, len(passive_params)))
wordlist = list(wordlist)
if args.casing:
delimiter, casing = detect_casing(args.casing)
wordlist = [covert_to_case(word, delimiter, casing) for word in wordlist]
else:
wordlist = list(wordlist)
except FileNotFoundError:
exit('%s The specified file for parameters doesn\'t exist' % bad)
@@ -118,11 +124,17 @@ def initialize(request, wordlist, single_url=False):
return 'skipped'
print('%s Probing the target for stability' % run)
request['url'] = stable_request(url, request['headers'])
mem.var['healthy_url'] = True
if not request['url']:
return 'skipped'
else:
fuzz = "z" + random_str(6)
response_1 = requester(request, {fuzz[:-1]: fuzz[::-1][:-1]})
if(isinstance(response_1, str)):
return 'skipped'
mem.var['healthy_url'] = response_1.status_code not in (400, 413, 418, 429, 503)
if not mem.var['healthy_url']:
print('%s Target returned HTTP %i, this may cause problems.' % (bad, response_1.status_code))
if single_url:
print('%s Analysing HTTP response for anomalies' % run)
response_2 = requester(request, {fuzz[:-1]: fuzz[::-1][:-1]})
@@ -139,16 +151,14 @@ def initialize(request, wordlist, single_url=False):
reason = compare(response_3, factors, {zzuf[:-1]: zzuf[::-1][:-1]})[2]
if not reason:
break
factors[reason] = False
if single_url:
print('%s Analysing HTTP response for potential parameter names' % run)
factors[reason] = None
if found:
num = len(found)
if words_exist:
print('%s Heuristic scanner found %i parameters' % (good, num))
print('%s Extracted %i parameters from response for testing' % (good, num))
else:
s = 's' if num > 1 else ''
print('%s Heuristic scanner found %i parameter%s: %s' % (good, num, s, ', '.join(found)))
print('%s Extracted %i parameter%s from response for testing: %s' % (good, num, s, ', '.join(found)))
if single_url:
print('%s Logicforcing the URL endpoint' % run)
populated = populate(wordlist)
@@ -182,16 +192,21 @@ def initialize(request, wordlist, single_url=False):
def main():
request = prepare_requests(args)
requests = prepare_requests(args)
final_result = {}
is_single = False if args.import_file else True
try:
if type(request) == dict:
# in case of a single target
mem.var['kill'] = False
mem.var['kill'] = False
count = 0
for request in requests:
url = request['url']
these_params = initialize(request, wordlist, single_url=True)
print('%s Scanning %d/%d: %s' % (run, count, len(requests), url))
these_params = initialize(request, wordlist, single_url=is_single)
count += 1
mem.var['kill'] = False
mem.var['bad_req_count'] = 0
if these_params == 'skipped':
print('%s Skipped %s due to errors' % (bad, url))
elif these_params:
@@ -199,34 +214,13 @@ def main():
final_result[url]['params'] = these_params
final_result[url]['method'] = request['method']
final_result[url]['headers'] = request['headers']
print('%s Parameters found: %s' % (good, ', '.join(final_result[url]['params'])))
exporter(final_result)
print('%s Parameters found: %-4s\n' % (good, ', '.join(final_result[url]['params'])))
if not mem.var['json_file']:
final_result = {}
continue
else:
print('%s No parameters were discovered.' % info)
elif type(request) == list:
# in case of multiple targets
count = 0
for each in request:
count += 1
url = each['url']
mem.var['kill'] = False
mem.var['bad_req_count'] = 0
print('%s Scanning %d/%d: %s' % (run, count, len(request), 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']
exporter(final_result)
print('%s Parameters found: %s\n' % (good, ', '.join(final_result[url]['params'])))
if not mem.var['json_file']:
final_result = {}
continue
else:
print('%s No parameters were discovered.\n' % info)
print('%s No parameters were discovered.\n' % info)
except KeyboardInterrupt:
exit()

View File

@@ -13,15 +13,15 @@ def define(response_1, response_2, param, value, wordlist):
returns dict
"""
factors = {
'same_code': False, # if http status code is same, contains that code
'same_body': False, # if http body is same, contains that body
'same_plaintext': False, # if http body isn't same but is same after removing html, contains that non-html text
'lines_num': False, # if number of lines in http body is same, contains that number
'lines_diff': False, # if http-body or plaintext aren't and there are more than two lines, contain which lines are same
'same_headers': False, # if the headers are same, contains those headers
'same_redirect': False, # if both requests redirect in similar manner, contains that redirection
'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
'same_code': None, # if http status code is same, contains that code
'same_body': None, # if http body is same, contains that body
'same_plaintext': None, # if http body isn't same but is same after removing html, contains that non-html text
'lines_num': None, # if number of lines in http body is same, contains that number
'lines_diff': None, # if http-body or plaintext aren't and there are more than two lines, contain which lines are same
'same_headers': None, # if the headers are same, contains those headers
'same_redirect': None, # if both requests redirect in similar manner, contains that redirection
'param_missing': None, # if param name is missing from the body, contains words that are already there
'value_missing': None # contains whether param value is missing from the body
}
if type(response_1) == type(response_2) == requests.models.Response:
body_1, body_2 = response_1.text, response_2.text
@@ -44,7 +44,7 @@ def define(response_1, response_2, param, value, wordlist):
elif remove_tags(body_1) == remove_tags(body_2):
factors['same_plaintext'] = remove_tags(body_1)
elif body_1 and body_2 and body_1.count('\\n') == body_2.count('\\n'):
factors['lines_diff'] = diff_map(body_1, body_2)
factors['lines_diff'] = diff_map(body_1, body_2)
if param not in response_2.text:
factors['param_missing'] = [word for word in wordlist if word in response_2.text]
if value not in response_2.text:
@@ -61,33 +61,33 @@ def compare(response, factors, params):
return ('', [], '')
these_headers = list(response.headers.keys())
these_headers.sort()
if factors['same_code'] and response.status_code != factors['same_code']:
if factors['same_code'] is not None and response.status_code != factors['same_code']:
return ('http code', params, 'same_code')
if factors['same_headers'] and these_headers != factors['same_headers']:
if factors['same_headers'] is not None and these_headers != factors['same_headers']:
return ('http headers', params, 'same_headers')
if mem.var['disable_redirects']:
if factors['same_redirect'] and urlparse(response.headers.get('Location', '')).path != factors['same_redirect']:
if factors['same_redirect'] is not None and urlparse(response.headers.get('Location', '')).path != factors['same_redirect']:
return ('redirection', params, 'same_redirect')
elif factors['same_redirect'] and 'Location' in response.headers:
elif factors['same_redirect'] is not None and 'Location' in response.headers:
if urlparse(response.headers.get('Location', '')).path != factors['same_redirect']:
return ('redirection', params, 'same_redirect')
if factors['same_body'] and response.text != factors['same_body']:
if factors['same_body'] is not None and response.text != factors['same_body']:
return ('body length', params, 'same_body')
if factors['lines_num'] and response.text.count('\n') != factors['lines_num']:
if factors['lines_num'] is not None and response.text.count('\n') != factors['lines_num']:
return ('number of lines', params, 'lines_num')
if factors['same_plaintext'] and remove_tags(response.text) != factors['same_plaintext']:
if factors['same_plaintext'] is not None and remove_tags(response.text) != factors['same_plaintext']:
return ('text length', params, 'same_plaintext')
if factors['lines_diff']:
if factors['lines_diff'] is not None:
for line in factors['lines_diff']:
if line not in response.text:
return ('lines', params, 'lines_diff')
if type(factors['param_missing']) == list:
if factors['param_missing'] is not None:
for param in params.keys():
if len(param) < 5:
continue
if param not in factors['param_missing'] and re.search(r'[\'"\s]%s[\'"\s]' % re.escape(param), response.text):
return ('param name reflection', params, 'param_missing')
if factors['value_missing']:
if factors['value_missing'] is not None:
for value in params.values():
if type(value) != str or len(value) != 6:
continue

View File

@@ -4,6 +4,7 @@ import arjun.core.config as mem
from arjun.core.colors import bad
def connection_refused():
"""
checks if a request should be retried if the server refused connection
@@ -17,6 +18,7 @@ def connection_refused():
print('%s Target has rate limiting in place, please use --stable switch' % bad)
return 'kill'
def error_handler(response, factors):
"""
decides what to do after performing a HTTP request
@@ -26,6 +28,8 @@ def error_handler(response, factors):
returns str
"""
if type(response) != str and response.status_code in (400, 413, 418, 429, 503):
if not mem.var['healthy_url']:
return 'ok'
if response.status_code == 503:
mem.var['kill'] = True
print('%s Target is unable to process requests, try --stable switch' % bad)

View File

@@ -6,6 +6,7 @@ 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
@@ -13,6 +14,7 @@ def json_export(result):
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
@@ -30,6 +32,7 @@ def burp_export(result):
elif data['method'] == 'JSON':
requests.post(url, json=populate(data['params']), headers=data['headers'], proxies=proxies, verify=False)
def text_export(result):
"""
exports results to a text file, one url per line
@@ -48,6 +51,7 @@ def text_export(result):
elif data['method'] == 'POST':
text_file.write(clean_url + '\t' + query_string + '\n')
def exporter(result):
"""
main exporter function that calls other export functions

View File

@@ -1,5 +1,18 @@
import re
burp_regex = re.compile(r'''(?m)^ <url><!\[CDATA\[(.+?)\]\]></url>
<host ip="[^"]*">[^<]+</host>
<port>[^<]*</port>
<protocol>[^<]*</protocol>
<method><!\[CDATA\[(.+?)\]\]></method>
<path>.*</path>
<extension>(.*)</extension>
<request base64="(?:false|true)"><!\[CDATA\[([\s\S]+?)]]></request>
<status>([^<]*)</status>
<responselength>([^<]*)</responselength>
<mimetype>([^<]*)</mimetype>''')
def reader(path, mode='string'):
"""
reads a file
@@ -11,6 +24,7 @@ def reader(path, mode='string'):
else:
return ''.join([line for line in file])
def parse_request(string):
"""
parses http request
@@ -25,6 +39,7 @@ def parse_request(string):
result['data'] = match.group(4)
return result
def parse_headers(string):
"""
parses headers
@@ -37,18 +52,6 @@ def parse_headers(string):
result[splitted[0]] = ':'.join(splitted[1:]).strip()
return result
burp_regex = re.compile(r'''(?m)^ <url><!\[CDATA\[(.+?)\]\]></url>
<host ip="[^"]*">[^<]+</host>
<port>[^<]*</port>
<protocol>[^<]*</protocol>
<method><!\[CDATA\[(.+?)\]\]></method>
<path>.*</path>
<extension>(.*)</extension>
<request base64="(?:false|true)"><!\[CDATA\[([\s\S]+?)]]></request>
<status>([^<]*)</status>
<responselength>([^<]*)</responselength>
<mimetype>([^<]*)</mimetype>''')
def burp_import(path):
"""
@@ -95,9 +98,11 @@ def urls_import(path, method, headers, include):
def request_import(path):
"""
imports request from a raw request file
returns dict
returns list
"""
return parse_request(reader(path))
result = []
result.append(parse_request(reader(path)))
return result
def importer(path, method, headers, include):
@@ -112,4 +117,4 @@ def importer(path, method, headers, include):
return urls_import(path, method, headers, include)
elif line.startswith(('GET', 'POST')):
return request_import(path)
return 'unknown'
return []

View File

@@ -1,6 +1,7 @@
import os
import tempfile
def prompt(default=None):
"""
lets user paste input by opening a temp file in a text editor

View File

@@ -11,6 +11,7 @@ from arjun.core.utils import dict_to_xml
warnings.filterwarnings('ignore') # Disable SSL related warnings
@sleep_and_retry
@limits(calls=mem.var['rate_limit'], period=1)
def requester(request, payload={}):

View File

@@ -153,7 +153,7 @@ def create_query_string(params):
pair = param + '=' + random_str(4) + '&'
query_string += pair
if query_string.endswith('&'):
query_string = query_string[:-1]
query_string = query_string[:-1]
return '?' + query_string
@@ -180,6 +180,7 @@ def extract_js(response):
scripts.append(actual_parts[0])
return scripts
def parse_headers(string):
"""
parses headers
@@ -248,7 +249,7 @@ def fetch_params(host):
def prepare_requests(args):
"""
creates a list of request objects used by Arjun from targets given by user
returns list (of targs)
returns list (of targets)
"""
headers = {
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:83.0) Gecko/20100101 Firefox/83.0',
@@ -258,6 +259,7 @@ def prepare_requests(args):
'Connection': 'close',
'Upgrade-Insecure-Requests': '1'
}
result = []
if type(args.headers) == str:
headers = extract_headers(args.headers)
elif args.headers:
@@ -266,15 +268,17 @@ def prepare_requests(args):
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
}
result.append(
{
'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 []
result = importer(args.import_file, mem.var['method'], headers, args.include)
return result
def nullify(*args, **kwargs):

View File

@@ -2,9 +2,10 @@ import requests
from urllib.parse import urlparse
def commoncrawl(host, page=0):
these_params = set()
response = requests.get('http://index.commoncrawl.org/CC-MAIN-2020-29-index?url=*.%s&fl=url&page=%s&limit=10000' % (host, page), verify=False).text
response = requests.get('http://index.commoncrawl.org/CC-MAIN-2024-42-index?url=*.%s&fl=url&page=%s&limit=10000' % (host, page), verify=False).text
if response.startswith('<!DOCTYPE html>'):
return ([], False, 'commoncrawl')
urls = response.split('\n')

View File

@@ -11,9 +11,11 @@ re_inputs = re.compile(r'''(?i)<(?:input|textarea)[^>]+?(?:id|name)=["']?([^"'\s
re_empty_vars = re.compile(r'''(?:[;\n]|\bvar|\blet)(\w+)\s*=\s*(?:['"`]{1,2}|true|false|null)''')
re_map_keys = re.compile(r'''['"](\w+?)['"]\s*:\s*['"`]''')
def is_not_junk(param):
return (re_not_junk.match(param) is not None)
def heuristic(raw_response, wordlist):
words_exist = False
potential_params = []

View File

@@ -2,6 +2,7 @@ import requests
from urllib.parse import urlparse
def otx(host, page):
these_params = set()
data = requests.get('https://otx.alienvault.com/api/v1/indicators/hostname/%s/url_list?limit=50&page=%d' % (host, page+1), verify=False).json()

View File

@@ -2,6 +2,7 @@ import requests
from urllib.parse import urlparse
def wayback(host, page):
payload = {
'url': host,

80
arjun/plugins/wl.py Normal file
View File

@@ -0,0 +1,80 @@
def detect_casing(string):
"""Detect the casing style and delimiter of given string."""
delimiter = ""
casing = ""
if string.islower():
casing = "l"
elif string.isupper():
casing = "u"
else:
casing = casing = "c" if string[0].islower() else "p"
if "-" in string:
delimiter = "-"
elif "_" in string:
delimiter = "_"
elif "." in string:
delimiter = "."
return delimiter, casing
def transform(parts, delimiter, casing):
"""Combine list of strings to form a string with given casing style."""
if len(parts) == 1:
if casing == "l":
return parts[0].lower()
elif casing == "u":
return parts[0].upper()
return parts[0]
result = []
for i, part in enumerate(parts):
if casing == "l":
transformed = part.lower()
elif casing == "u":
transformed = part.upper()
elif casing == "c":
if i == 0:
transformed = part.lower()
else:
transformed = part.lower().title()
else: # casing == "p"
transformed = part.lower().title()
result.append(transformed)
return delimiter.join(result)
def handle(text):
"""Break down a string into array of 'words'."""
if "-" in text:
return text.split("-")
elif "_" in text:
return text.split("_")
elif "." in text:
return text.split(".")
if not text.islower() and not text.isupper():
parts = []
temp = ""
for char in text:
if not char.isupper():
temp += char
else:
if temp:
parts.append(temp)
temp = char
if temp:
parts.append(temp)
return parts
return [text]
def covert_to_case(string, delimiter, casing):
"""Process input stream and write transformed text to output stream."""
parts = handle(string)
return transform(parts, delimiter, casing)