Skip to content

Commit e57b5e2

Browse files
committed
version 1.0.1
1 parent 570c4ae commit e57b5e2

8 files changed

+148
-47
lines changed

README.md

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# <img src="https://github.com/nxenon/h2spacex/assets/61124903/fd6387bf-15e8-4a5d-816b-cf5e079e07cc" width="20%" valign="middle" alt="H2SpaceX" />&nbsp;&nbsp; H2SpaceX
22

3-
[![pypi: 0.1.17](https://img.shields.io/badge/pypi-0.1.17-8c34eb.svg)](https://pypi.org/project/h2spacex/)
3+
[![pypi: 1.0.1](https://img.shields.io/badge/pypi-1.0.1-8c34eb.svg)](https://pypi.org/project/h2spacex/)
44
[![Python: 3.10](https://img.shields.io/badge/Python->=3.10-blue.svg)](https://www.python.org)
55
[![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-006112.svg)](https://github.com/nxenon/h2spacex/blob/main/LICENSE)
66

@@ -46,6 +46,14 @@ H2SpaceX works with Python 3 (preferred: >=3.10)
4646

4747
pip install h2spacex
4848

49+
50+
## Error in Installation
51+
if you get errors of scapy:
52+
53+
54+
pip install --upgrade scapy
55+
56+
4957
# Quick Start
5058
You can import the HTTP/2 TLS Connection and set up the connection. After setting up the connection, you can do other things:
5159

@@ -67,6 +75,17 @@ See examples which contain some Portswigger race condition examples.
6775

6876
[Examples Page](./examples)
6977

78+
# Improved Single Packet Attack Method (Black Hat 2024)
79+
James Kettle introduced a improved version of Single Packet Attack in Black Hat 2024:
80+
81+
![Impvoved Version Image](https://github.com/user-attachments/assets/bf7bf88c-937a-4a95-899b-990bc6fc6a23)
82+
83+
You can implement this method easily using `send_ping_frame()` method.
84+
85+
[Improved Version of SPA Sample Exploit](./examples/improved-spa-method.py)
86+
## Reference of Improved Method:
87+
- [Listen to the whispers: web timing attacks that actually work](https://portswigger.net/research/listen-to-the-whispers-web-timing-attacks-that-actually-work)
88+
7089
# References & Resources
7190

7291
- [James Kettle DEF CON 31 Presentation](https://youtu.be/tKJzsaB1ZvI?si=6uAuzOt3wjnEGYP6)

examples/improved-spa-method.py

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from h2spacex import H2OnTlsConnection
2+
from time import sleep
3+
from h2spacex import h2_frames
4+
5+
h2_conn = H2OnTlsConnection(
6+
hostname='http2.github.io',
7+
port_number=443
8+
)
9+
10+
headers = """accept: */*
11+
content-type: application/x-www-form-urlencoded
12+
...
13+
"""
14+
15+
body = """BODY
16+
DATA...
17+
...
18+
"""
19+
stream_ids_list = h2_conn.generate_stream_ids(number_of_streams=5)
20+
21+
all_headers_frames = [] # all headers frame + data frames which have not the last byte
22+
all_data_frames = [] # all data frames which contain the last byte
23+
24+
for s_id in stream_ids_list:
25+
header_frames_without_last_byte, last_data_frame_with_last_byte = h2_conn.create_single_packet_http2_post_request_frames(
26+
method='POST',
27+
headers_string=headers,
28+
scheme='https',
29+
stream_id=s_id,
30+
authority="http2.github.io",
31+
body=body,
32+
path='/somePath'
33+
)
34+
35+
all_headers_frames.append(header_frames_without_last_byte)
36+
all_data_frames.append(last_data_frame_with_last_byte)
37+
38+
# concatenate all headers bytes
39+
temp_headers_bytes = b''
40+
for h in all_headers_frames:
41+
temp_headers_bytes += bytes(h)
42+
43+
# concatenate all data frames which have last byte
44+
temp_data_bytes = b''
45+
for d in all_data_frames:
46+
temp_data_bytes += bytes(d)
47+
48+
h2_conn.setup_connection()
49+
h2_conn.send_ping_frame() # important line (in improved version of single packet attack)
50+
51+
# send header frames
52+
h2_conn.send_frames(temp_headers_bytes)
53+
54+
# wait some time
55+
sleep(0.1)
56+
57+
# send ping frame to warm up connection
58+
h2_conn.send_ping_frame()
59+
60+
# send remaining data frames
61+
h2_conn.send_frames(temp_data_bytes)
62+
63+
# parse response frames
64+
resp = h2_conn.read_response_from_socket(_timeout=3)
65+
frame_parser = h2_frames.FrameParser(h2_connection=h2_conn)
66+
frame_parser.add_frames(resp)
67+
frame_parser.show_response_of_sent_requests()
68+
69+
# close the connection to stop response parsing and exit the script
70+
h2_conn.close_connection()

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "h2spacex"
7-
version = "0.1.17"
7+
version = "1.0.1"
88
authors = [
99
{ name="nxenon", email="[email protected]" },
1010
]

setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name='h2spacex',
8-
version='0.1.17',
8+
version='1.0.1',
99
description='HTTP/2 Single Packet Attack low level library based on Scapy',
1010
package_dir={"": "src"},
1111
packages=find_packages(where="src"),

src/h2spacex/h2_connection.py

+11-9
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from scapy.all import hex_bytes
88
from . import h2_frames, utils
99
import socks
10+
from .modules.logger import Logger
11+
logger = Logger()
1012

1113

1214
class H2Connection:
@@ -89,7 +91,7 @@ def _create_socks_socket(self):
8991
sock.connect((self.hostname, self.port_number))
9092
self.raw_socket = sock
9193
sock_addr = sock.getsockname()
92-
print(f'+ Connected through Proxy: {self.hostname}:{self.port_number} --> {sock_addr[0]}:{sock_addr[1]}')
94+
logger.logger_print(f'+ Connected through Proxy: {self.hostname}:{self.port_number} --> {sock_addr[0]}:{sock_addr[1]}')
9395

9496
def _create_raw_socket(self):
9597
"""
@@ -108,11 +110,11 @@ def _create_raw_socket(self):
108110

109111
self.raw_socket = raw_socket
110112
sock_addr = raw_socket.getsockname()
111-
print(f'+ Connected to: {self.hostname}:{self.port_number} --> {sock_addr[0]}:{sock_addr[1]}')
113+
logger.logger_print(f'+ Connected to: {self.hostname}:{self.port_number} --> {sock_addr[0]}:{sock_addr[1]}')
112114

113115
def _send_h2_connection_preface(self):
114116
self.send_bytes(self.H2_PREFACE)
115-
print('+ H2 connection preface sent')
117+
logger.logger_print('+ H2 connection preface sent')
116118

117119
def get_using_socket(self):
118120
"""
@@ -131,7 +133,7 @@ def send_bytes(self, bytes_data: bytes):
131133
try:
132134
using_socket.send(bytes_data)
133135
except Exception as e:
134-
print('# Error in sending bytes: ' + str(e))
136+
logger.logger_print('# Error in sending bytes: ' + str(e))
135137

136138
def send_frames(self, frames):
137139
"""
@@ -221,10 +223,10 @@ def _send_client_initial_settings_frame(self):
221223
client_initial_settings_frame = h2_frames.create_settings_frame(settings=settings_list)
222224

223225
self.send_bytes(bytes(client_initial_settings_frame))
224-
print('+ Client initial SETTINGS frame sent: ')
225-
print('// Client SETTINGS //')
226-
print(self.DEFAULT_SETTINGS)
227-
print()
226+
logger.logger_print('+ Client initial SETTINGS frame sent: ')
227+
logger.logger_print('// Client SETTINGS //')
228+
logger.logger_print(self.DEFAULT_SETTINGS)
229+
logger.logger_print()
228230

229231
def generate_stream_ids(self, number_of_streams):
230232
"""
@@ -441,4 +443,4 @@ def send_simple_http2_request(
441443
{body}
442444
+----- END Request Info -----+
443445
"""
444-
print(more_info_msg)
446+
logger.logger_print(more_info_msg)

src/h2spacex/h2_frames.py

+33-31
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import gzip
44
import brotli
55
import zlib
6+
from .modules.logger import Logger
7+
logger = Logger()
68

79

810
def decompress_gzip_data(gzip_data: bytes):
@@ -11,7 +13,7 @@ def decompress_gzip_data(gzip_data: bytes):
1113
decompressed_content = gzip.decompress(gzip_data)
1214
decoded_content = decompressed_content.decode('utf-8')
1315
except Exception as e:
14-
print('# Error in decompressing gzip encoded body : ' + str(e))
16+
logger.logger_print('# Error in decompressing gzip encoded body : ' + str(e))
1517
return gzip_data
1618

1719
return decoded_content
@@ -23,7 +25,7 @@ def decompress_br_data(br_data: bytes):
2325
decompressed_content = brotli.decompress(br_data)
2426
decoded_content = decompressed_content.decode('utf-8')
2527
except Exception as e:
26-
print('# Error in decompressing br encoded body : ' + str(e))
28+
logger.logger_print('# Error in decompressing br encoded body : ' + str(e))
2729
return br_data
2830

2931
return decoded_content
@@ -35,7 +37,7 @@ def decompress_deflate_data(deflate_data: bytes):
3537
decompressed_content = zlib.decompress(deflate_data, -zlib.MAX_WBITS)
3638
decoded_content = decompressed_content.decode('utf-8')
3739
except Exception as e:
38-
print('# Error in decompressing deflate encoded body : ' + str(e))
40+
logger.logger_print('# Error in decompressing deflate encoded body : ' + str(e))
3941
return deflate_data
4042

4143
return decoded_content
@@ -78,7 +80,7 @@ def add_frames(self, frames_bytes: bytes, is_verbose=False):
7880

7981
for f in parsed_frames:
8082
if is_verbose:
81-
print(f.show())
83+
logger.logger_print(f.show())
8284

8385
if isinstance(f.payload, h2.H2HeadersFrame):
8486
self.parse_header_frame(f)
@@ -103,33 +105,33 @@ def add_frames(self, frames_bytes: bytes, is_verbose=False):
103105
self.parse_reset_frame(f)
104106

105107
else:
106-
print('--frame--')
107-
print('Frame Type: ' + str(type(f.payload)) + ' / Type ID: ' + str(f.type))
108+
logger.logger_print('--frame--')
109+
logger.logger_print('Frame Type: ' + str(type(f.payload)) + ' / Type ID: ' + str(f.type))
108110
f.show()
109-
print('##frame##')
111+
logger.logger_print('##frame##')
110112

111113
def parse_settings_frame(self, settings_frame):
112114
if 'A' in settings_frame.flags:
113-
print('* Server sent ACK for client SETTINGS frame')
115+
logger.logger_print('* Server sent ACK for client SETTINGS frame')
114116

115117
else:
116-
print('* Server sent Settings frame with following values:')
117-
print('// Server SETTINGS //')
118-
print(settings_frame.settings)
119-
print()
118+
logger.logger_print('* Server sent Settings frame with following values:')
119+
logger.logger_print('// Server SETTINGS //')
120+
logger.logger_print(settings_frame.settings)
121+
logger.logger_print()
120122
ack_settings_frame = create_settings_frame(is_ack=1)
121123
self.send_frame(ack_settings_frame)
122-
print('+ Client sent ACK for server SETTINGS frame')
124+
logger.logger_print('+ Client sent ACK for server SETTINGS frame')
123125

124126
def parse_window_update_frame(self, windows_update_frame):
125-
print('* Server sent WINDOW UPDATE frame with win_increase_size of: ' + str(windows_update_frame.win_size_incr))
127+
logger.logger_print('* Server sent WINDOW UPDATE frame with win_increase_size of: ' + str(windows_update_frame.win_size_incr))
126128

127129
def parse_ping_frame(self, ping_frame):
128130
if 'A' in ping_frame.flags:
129-
print('* Server sent ACK for PING frame')
131+
logger.logger_print('* Server sent ACK for PING frame')
130132

131133
def parse_reset_frame(self, reset_frame):
132-
print(f'# Server sent RESET frame for Stream ID: {reset_frame.stream_id}, with Err_Code: {reset_frame.error}')
134+
logger.logger_print(f'# Server sent RESET frame for Stream ID: {reset_frame.stream_id}, with Err_Code: {reset_frame.error}')
133135

134136
def parse_header_frame(self, header_frame):
135137
headers_string = self.get_headers_string_from_headers_frame(header_frame)
@@ -259,56 +261,56 @@ def parse_response_frames_bytes(
259261

260262
raw_frame_bytes = frames_bytes
261263
if raw_frame_bytes:
262-
print('+--------- START Response Frames ---------+')
264+
logger.logger_print('+--------- START Response Frames ---------+')
263265
parsed_frames = h2.H2Seq(raw_frame_bytes).frames
264266
# print(parsed_frames)
265267

266268
for f in parsed_frames:
267269
if is_verbose:
268-
print(f.show())
270+
logger.logger_print(f.show())
269271

270272
if isinstance(f.payload, h2.H2HeadersFrame):
271273
headers_string = _get_headers_string_from_headers_frame(f)
272-
print(f'------ Headers Stream ID: {f.stream_id} ------')
273-
print(headers_string)
274+
logger.logger_print(f'------ Headers Stream ID: {f.stream_id} ------')
275+
logger.logger_print(headers_string)
274276

275277
elif isinstance(f.payload, h2.H2DataFrame):
276-
print(f'------ Data Stream ID: {f.stream_id} ------')
278+
logger.logger_print(f'------ Data Stream ID: {f.stream_id} ------')
277279
with gzip.GzipFile(fileobj=gzip.io.BytesIO(f.data), mode='rb') as decompressed_file:
278280
# Read the decompressed data
279281
decompressed_content = decompressed_file.read()
280282

281283
# If the decompressed content is in bytes, you might want to decode it (if it contains text)
282284
decoded_content = decompressed_content.decode('utf-8')
283-
print(decoded_content)
285+
logger.logger_print(decoded_content)
284286

285287
# print(f'------ Data Stream ID: {f.stream_id} ------')
286288
# print(str(f.data))
287289

288290
elif isinstance(f.payload, h2.H2SettingsFrame):
289291
if socket_obj is not None:
290-
print('* got a Settings frame from server')
292+
logger.logger_print('* got a Settings frame from server')
291293
settings_frame = create_settings_frame(is_ack=1)
292294
socket_obj.send(bytes(settings_frame))
293-
print('* client sent ACK for server Settings')
295+
logger.logger_print('* client sent ACK for server Settings')
294296

295297
elif isinstance(f.payload, h2.H2WindowUpdateFrame):
296-
print('* server sent WindowUpdate Frame with win_increase_size of: ' + str(f.win_size_incr))
298+
logger.logger_print('* server sent WindowUpdate Frame with win_increase_size of: ' + str(f.win_size_incr))
297299

298300
elif isinstance(f.payload, h2.H2PingFrame):
299-
print('* server sent ACK for PING frame')
301+
logger.logger_print('* server sent ACK for PING frame')
300302

301303
elif isinstance(f.payload, NoPayload):
302304
if f.type == 4: # settings frame
303305
if 'A' in f.flags:
304-
print('* server sent ACK for client Settings')
306+
logger.logger_print('* server sent ACK for client Settings')
305307

306308
else:
307-
print('--frame--')
309+
logger.logger_print('--frame--')
308310
f.show()
309-
print('##frame##')
311+
logger.logger_print('##frame##')
310312

311-
print('+--------- END Response Frames ---------+')
313+
logger.logger_print('+--------- END Response Frames ---------+')
312314

313315

314316
def create_ping_frame(ping_data='12345678', is_ack=0):
@@ -318,7 +320,7 @@ def create_ping_frame(ping_data='12345678', is_ack=0):
318320
"""
319321

320322
if len(ping_data) != 8:
321-
print('ping frame payload must be 8 in length! --> ' + ping_data + ' is invalid!')
323+
logger.logger_print('ping frame payload must be 8 in length! --> ' + ping_data + ' is invalid!')
322324
exit()
323325

324326
if is_ack:

0 commit comments

Comments
 (0)