507 lines
16 KiB
Python
507 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
#
|
|
#######################################################################
|
|
#
|
|
# A script to paste to https://cpaste.org/
|
|
#
|
|
# Copyright (c) 2013-2019 Andreas Schneider <asn@samba.org>
|
|
# Copyright (c) 2013 Alexander Bokovoy <ab@samba.org>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
#######################################################################
|
|
#
|
|
# Requires: python3-requests
|
|
# Requires: python3-cryptography
|
|
#
|
|
# Optionally requires: python-Pygments
|
|
#
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import base64
|
|
import zlib
|
|
import requests
|
|
import html
|
|
import random
|
|
from bs4 import BeautifulSoup
|
|
from packaging import version as pkg_version
|
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
from cryptography.hazmat.primitives import hashes
|
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
from cryptography.hazmat.backends import default_backend
|
|
from optparse import OptionParser
|
|
from mimetypes import guess_type
|
|
try:
|
|
from pygments.lexers import guess_lexer, guess_lexer_for_filename
|
|
from pygments.util import ClassNotFound
|
|
guess_lang = True
|
|
except ImportError:
|
|
guess_lang = False
|
|
|
|
|
|
def base58_encode(v: bytes):
|
|
# 58 char alphabet
|
|
alphabet = b'123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
|
|
alphabet_len = len(alphabet)
|
|
|
|
nPad = len(v)
|
|
v = v.lstrip(b'\0')
|
|
nPad -= len(v)
|
|
|
|
x = 0
|
|
for (i, c) in enumerate(v[::-1]):
|
|
if isinstance(c, str):
|
|
c = ord(c)
|
|
x += c << (8 * i)
|
|
|
|
string = b''
|
|
while x:
|
|
x, idx = divmod(x, alphabet_len)
|
|
string = alphabet[idx:idx+1] + string
|
|
|
|
return (alphabet[0:1] * nPad + string)
|
|
|
|
|
|
def json_encode(d):
|
|
return json.dumps(d, separators=(',', ':')).encode('utf-8')
|
|
|
|
|
|
#
|
|
# The encryption format is described here:
|
|
# https://github.com/PrivateBin/PrivateBin/wiki/Encryption-format
|
|
#
|
|
def privatebin_encrypt(paste_passphrase,
|
|
paste_password,
|
|
paste_plaintext,
|
|
paste_formatter,
|
|
paste_attachment_name,
|
|
paste_attachment,
|
|
paste_compress,
|
|
paste_burn,
|
|
paste_opendicussion):
|
|
if paste_password:
|
|
paste_passphrase += bytes(paste_password, 'utf-8')
|
|
|
|
# PBKDF
|
|
kdf_salt = bytes(os.urandom(8))
|
|
kdf_iterations = 100000
|
|
kdf_keysize = 256 # size of resulting kdf_key
|
|
|
|
backend = default_backend()
|
|
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(),
|
|
length=int(kdf_keysize / 8), # 256bit
|
|
salt=kdf_salt,
|
|
iterations=kdf_iterations,
|
|
backend=backend)
|
|
kdf_key = kdf.derive(paste_passphrase)
|
|
|
|
# AES-GCM
|
|
adata_size = 128
|
|
|
|
cipher_iv = bytes(os.urandom(int(adata_size / 8)))
|
|
cipher_algo = "aes"
|
|
cipher_mode = "gcm"
|
|
|
|
compression_type = "none"
|
|
if paste_compress:
|
|
compression_type = "zlib"
|
|
|
|
# compress plaintext
|
|
paste_data = {'paste': paste_plaintext}
|
|
if paste_attachment_name and paste_attachment:
|
|
paste_data['attachment'] = paste_attachment
|
|
paste_data['attachment_name'] = paste_attachment_name
|
|
print(paste_attachment_name)
|
|
print(paste_attachment)
|
|
|
|
if paste_compress:
|
|
zobj = zlib.compressobj(wbits=-zlib.MAX_WBITS)
|
|
paste_blob = zobj.compress(json_encode(paste_data)) + zobj.flush()
|
|
else:
|
|
paste_blob = json_encode(paste_data)
|
|
|
|
# Associated data to authenticate
|
|
paste_adata = [
|
|
[
|
|
base64.b64encode(cipher_iv).decode("utf-8"),
|
|
base64.b64encode(kdf_salt).decode("utf-8"),
|
|
kdf_iterations,
|
|
kdf_keysize,
|
|
adata_size,
|
|
cipher_algo,
|
|
cipher_mode,
|
|
compression_type,
|
|
],
|
|
paste_formatter,
|
|
int(paste_opendicussion),
|
|
int(paste_burn),
|
|
]
|
|
|
|
paste_adata_json = json_encode(paste_adata)
|
|
|
|
aesgcm = AESGCM(kdf_key)
|
|
ciphertext = aesgcm.encrypt(cipher_iv, paste_blob, paste_adata_json)
|
|
|
|
# Validate
|
|
# aesgcm.decrypt(cipher_iv, ciphertext, paste_adata_json)
|
|
|
|
paste_ciphertext = base64.b64encode(ciphertext).decode("utf-8")
|
|
|
|
return paste_adata, paste_ciphertext
|
|
|
|
|
|
def privatebin_send(paste_url,
|
|
paste_password,
|
|
paste_plaintext,
|
|
paste_formatter,
|
|
paste_attachment_name,
|
|
paste_attachment,
|
|
paste_compress,
|
|
paste_burn,
|
|
paste_opendicussion,
|
|
paste_expire):
|
|
paste_passphrase = bytes(os.urandom(32))
|
|
|
|
paste_adata, paste_ciphertext = privatebin_encrypt(paste_passphrase,
|
|
paste_password,
|
|
paste_plaintext,
|
|
paste_formatter,
|
|
paste_attachment_name,
|
|
paste_attachment,
|
|
paste_compress,
|
|
paste_burn,
|
|
paste_opendicussion)
|
|
|
|
# json payload for the post API
|
|
# https://github.com/PrivateBin/PrivateBin/wiki/API
|
|
payload = {
|
|
"v": 2,
|
|
"adata": paste_adata,
|
|
"ct": paste_ciphertext,
|
|
"meta": {
|
|
"expire": paste_expire,
|
|
}
|
|
}
|
|
|
|
# http content type
|
|
headers = {'X-Requested-With': 'JSONHttpRequest'}
|
|
|
|
r = requests.post(paste_url,
|
|
data=json_encode(payload),
|
|
headers=headers)
|
|
r.raise_for_status()
|
|
|
|
try:
|
|
result = r.json()
|
|
except:
|
|
print('Oops, error: %s' % (r.text))
|
|
sys.exit(1)
|
|
|
|
paste_status = result['status']
|
|
if paste_status:
|
|
paste_message = result['message']
|
|
print("Oops, error: %s" % paste_message)
|
|
sys.exit(1)
|
|
|
|
paste_id = result['id']
|
|
paste_url_id = result['url']
|
|
paste_deletetoken = result['deletetoken']
|
|
|
|
# print('Delete paste: %s/?pasteid=%s&deletetoken=%s' %
|
|
# (paste_url, paste_id, paste_deletetoken))
|
|
# print('')
|
|
# print('### Paste (%s): %s%s#%s' %
|
|
# (paste_formatter,
|
|
# paste_url,
|
|
# paste_url_id,
|
|
# base58_encode(paste_passphrase).decode('utf-8')))
|
|
|
|
return paste_url, paste_url_id, base58_encode(paste_passphrase).decode('utf-8'), paste_deletetoken
|
|
|
|
|
|
|
|
def guess_lang_formatter(paste_plaintext, paste_filename=None):
|
|
paste_formatter = 'plaintext'
|
|
|
|
# Map numpy to python because the numpy lexer gives false positives
|
|
# when guessing.
|
|
lexer_lang_map = {'numpy': 'python'}
|
|
|
|
# If we have a filename, try guessing using the more reliable
|
|
# guess_lexer_for_filename function.
|
|
# If that fails, try the guess_lexer function on the code.
|
|
lang = None
|
|
if paste_filename:
|
|
try:
|
|
lang = guess_lexer_for_filename(paste_filename,
|
|
paste_plaintext).name.lower()
|
|
except ClassNotFound:
|
|
print("No guess by filename")
|
|
pass
|
|
else:
|
|
try:
|
|
lang = guess_lexer(paste_plaintext).name.lower()
|
|
except ClassNotFound:
|
|
pass
|
|
|
|
if lang:
|
|
if lang == 'markdown':
|
|
paste_formatter = 'markdown'
|
|
if lang != 'text only':
|
|
paste_formatter = 'syntaxhighlighting'
|
|
|
|
return paste_formatter
|
|
|
|
|
|
def main():
|
|
parser = OptionParser()
|
|
|
|
parser.add_option("-f", "--file", dest="filename",
|
|
help="Read from a file instead of stdin",
|
|
metavar="FILE")
|
|
parser.add_option("-p", "--password", dest="password",
|
|
help="Create a password protected paste",
|
|
metavar="PASSWORD")
|
|
parser.add_option("-e", "--expire",
|
|
action="store", dest="expire", default="1day",
|
|
choices=["5min",
|
|
"10min",
|
|
"1hour",
|
|
"1day",
|
|
"1week",
|
|
"1month",
|
|
"1year",
|
|
"never"],
|
|
help="Expiration time of the paste (default: 1day)")
|
|
parser.add_option("-s", "--sourcecode",
|
|
action="store_true", dest="source", default=False,
|
|
help="Use source code highlighting")
|
|
parser.add_option("-m", "--markdown",
|
|
action="store_true", dest="markdown", default=False,
|
|
help="Parse paste as markdown")
|
|
parser.add_option("-b", "--burn",
|
|
action="store_true", dest="burn", default=False,
|
|
help="Burn paste after reading")
|
|
parser.add_option("-o", "--opendiscussion",
|
|
action="store_true", dest="opendiscussion",
|
|
default=False,
|
|
help="Allow discussion for the paste")
|
|
parser.add_option("-a", "--attachment", dest="attachment",
|
|
help="Specify path to a file to attachment to the paste",
|
|
metavar="FILE")
|
|
|
|
(options, args) = parser.parse_args()
|
|
|
|
paste_url = 'https://cpaste.org'
|
|
paste_formatter = 'plaintext'
|
|
paste_compress = True
|
|
paste_expire = '1day'
|
|
paste_opendiscussion = 0
|
|
paste_burn = 0
|
|
paste_password = None
|
|
paste_attachment_name = None
|
|
paste_attachment = None
|
|
|
|
if options.filename:
|
|
f = open(options.filename)
|
|
if not f:
|
|
print("Oops, could not open file!")
|
|
|
|
paste_plaintext = f.read()
|
|
f.close()
|
|
else:
|
|
paste_plaintext = sys.stdin.read()
|
|
|
|
if not paste_plaintext:
|
|
print("Oops, we have no data")
|
|
sys.exit(1)
|
|
|
|
if options.burn:
|
|
paste_burn = 1
|
|
|
|
if options.opendiscussion:
|
|
paste_opendiscussion = 1
|
|
|
|
if options.source:
|
|
paste_formatter = 'syntaxhighlighting'
|
|
elif options.markdown:
|
|
paste_formatter = 'markdown'
|
|
elif guess_lang:
|
|
paste_formatter = guess_lang_formatter(paste_plaintext,
|
|
options.filename)
|
|
|
|
if options.expire:
|
|
paste_expire = options.expire
|
|
|
|
if options.password:
|
|
paste_password = options.password
|
|
|
|
if options.attachment:
|
|
paste_attachment_name = os.path.basename(options.attachment)
|
|
mime = guess_type(options.attachment, strict=False)[0]
|
|
if not mime:
|
|
mime = 'application/octet-stream'
|
|
|
|
f = open(options.attachment, mode='rb')
|
|
if not f:
|
|
print("Oops, could not open file for attachment!")
|
|
|
|
data = f.read()
|
|
f.close()
|
|
|
|
paste_attachment = 'data:%s;base64,' % (mime)
|
|
paste_attachment += base64.b64encode(data).decode('utf-8')
|
|
|
|
print(paste_url)
|
|
print(paste_password )
|
|
print(paste_plaintext )
|
|
print(paste_formatter )
|
|
print(paste_attachment_name )
|
|
print(paste_attachment )
|
|
print(paste_compress )
|
|
print(paste_burn )
|
|
print(paste_opendiscussion )
|
|
print(paste_expire )
|
|
|
|
privatebin_send(paste_url,
|
|
paste_password,
|
|
paste_plaintext,
|
|
paste_formatter,
|
|
paste_attachment_name,
|
|
paste_attachment,
|
|
paste_compress,
|
|
paste_burn,
|
|
paste_opendiscussion,
|
|
paste_expire)
|
|
|
|
sys.exit(0)
|
|
|
|
def publish_to_cpaste(file, paste_url='https://cpaste.org'):
|
|
#paste_url = 'https://cpaste.org'
|
|
#paste_url = 'https://paste.devsite.pl'
|
|
paste_formatter = 'plaintext'
|
|
paste_compress = True
|
|
paste_expire = '1day'
|
|
paste_opendiscussion = 0
|
|
paste_burn = 0
|
|
paste_password = None
|
|
paste_attachment_name = None
|
|
paste_attachment = None
|
|
|
|
f = open(file)
|
|
if not f:
|
|
print("Oops, could not open file!")
|
|
|
|
paste_plaintext = f.read()
|
|
f.close()
|
|
|
|
paste_url, paste_id, paste_decrypt, paste_deletetoken = privatebin_send(paste_url,
|
|
paste_password,
|
|
paste_plaintext,
|
|
paste_formatter,
|
|
paste_attachment_name,
|
|
paste_attachment,
|
|
paste_compress,
|
|
paste_burn,
|
|
paste_opendiscussion,
|
|
paste_expire)
|
|
|
|
output = '%s%s#%s DELETE TOKEN: %s' % (paste_url, paste_id, paste_decrypt, paste_deletetoken)
|
|
return output
|
|
|
|
def publish_to_multiple_cpastes(file, num_instances=3, max_attempts=5):
|
|
instances = get_list_of_instances()
|
|
results = {}
|
|
attempts = 0
|
|
|
|
while len(results) < num_instances and attempts < max_attempts:
|
|
# Randomly select an instance that hasn't been tried yet
|
|
available_instances = [i for i in instances if i not in results]
|
|
if not available_instances:
|
|
break # No more instances to try
|
|
|
|
instance = random.choice(available_instances)
|
|
attempts += 1
|
|
|
|
try:
|
|
# Extract the base URL from the instance link
|
|
base_url = instance.split('?')[0]
|
|
result = publish_to_cpaste(file, base_url)
|
|
results[base_url] = result
|
|
except Exception as e:
|
|
print(f"Failed to publish to {instance}: {str(e)}")
|
|
# We don't add this instance to results, so it might be retried
|
|
|
|
if len(results) < num_instances:
|
|
print(f"Warning: Only managed to publish to {len(results)} instances out of {num_instances} requested.")
|
|
|
|
return results
|
|
|
|
def get_list_of_instances():
|
|
|
|
# Function to fetch HTML content from a URL
|
|
def fetch_html_from_url(url):
|
|
response = requests.get(url)
|
|
response.raise_for_status() # Raise an HTTPError for bad responses
|
|
return response.text
|
|
|
|
# Function to find the highest version number
|
|
def find_highest_version(soup):
|
|
versions = set()
|
|
rows = soup.find_all('tr', class_='opacity4')
|
|
|
|
for row in rows:
|
|
version_td = row.find_all('td')[1].text.strip()
|
|
versions.add(version_td)
|
|
|
|
return max(versions, key=pkg_version.parse)
|
|
|
|
# Function to extract and decode URLs of the latest version
|
|
def extract_latest_version_links(soup, highest_version):
|
|
rows = soup.find_all('tr', class_='opacity4')
|
|
latest_version_links = []
|
|
|
|
for row in rows:
|
|
version_td = row.find_all('td')[1].text.strip()
|
|
if version_td == highest_version:
|
|
href = row.find('a')['href']
|
|
decoded_href = html.unescape(href)
|
|
latest_version_links.append(decoded_href)
|
|
|
|
return latest_version_links
|
|
|
|
# Specify the URL to fetch the HTML content
|
|
url = 'https://privatebin.info/directory/' # Replace with your actual URL
|
|
|
|
# Fetch the HTML content from the URL
|
|
html_content = fetch_html_from_url(url)
|
|
|
|
# Parse the HTML content with BeautifulSoup
|
|
soup = BeautifulSoup(html_content, 'lxml')
|
|
|
|
# Find the highest version number
|
|
highest_version = find_highest_version(soup)
|
|
print(f'Highest version found: {highest_version}')
|
|
|
|
# Extract and print the latest version links
|
|
latest_version_links = extract_latest_version_links(soup, highest_version)
|
|
return latest_version_links
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|