Skip to content

Commit e258345

Browse files
committed
Add tube.upload_manually
Upload data in chunks when having a tube connected to a shell. This is useful when doing kernel or qemu challenges where you can't use the ssh tube's file upload features.
1 parent 27366fd commit e258345

File tree

1 file changed

+88
-0
lines changed

1 file changed

+88
-0
lines changed

pwnlib/tubes/tube.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
from pwnlib.log import Logger
2222
from pwnlib.timeout import Timeout
2323
from pwnlib.tubes.buffer import Buffer
24+
from pwnlib.util import fiddling
25+
from pwnlib.util import iters
2426
from pwnlib.util import misc
2527
from pwnlib.util import packing
2628

@@ -1077,6 +1079,92 @@ def clean_and_log(self, timeout = 0.05):
10771079
with context.local(log_level='debug'):
10781080
return cached_data + self.clean(timeout)
10791081

1082+
def upload_manually(self, data, target_path = './payload', prompt = b'$', chunk_size = 0x200, chmod_flags = 'u+x', compression='auto', end_marker = 'PWNTOOLS_DONE'):
1083+
"""upload_manually(data, target_path = './payload', prompt = b'$', chunk_size = 0x200, chmod_flags = 'u+x', compression='auto', end_marker = 'PWNTOOLS_DONE')
1084+
1085+
Upload a file manually using base64 encoding and compression.
1086+
This can be used when the tube is connected to a shell.
1087+
1088+
The file is uploaded in base64-encoded chunks by appending to a file
1089+
and then decompressing it:
1090+
1091+
```
1092+
loop:
1093+
echo <chunk> | base64 -d >> <target_path>.<compression>
1094+
<compression> -d <target_path>.<compression>
1095+
chmod <chmod_flags> <target_path>
1096+
```
1097+
1098+
It is assumed that a `base64` command is available on the target system.
1099+
When ``compression`` is ``auto`` the best compression utility available is chosen.
1100+
1101+
Arguments:
1102+
1103+
data(bytes): The data to upload.
1104+
target_path(str): The path to upload the data to.
1105+
prompt(bytes): The shell prompt to wait for.
1106+
chunk_size(int): The size of each chunk to upload.
1107+
chmod_flags(str): The flags to use with chmod. ``""`` to ignore.
1108+
compression(str): The compression to use. ``auto`` to automatically choose the best compression or ``gzip`` or ``xz``.
1109+
end_marker(str): The marker to use to detect the end of the output. Only used when prompt is not set.
1110+
1111+
"""
1112+
echo_end = ""
1113+
if not prompt:
1114+
echo_end = "; echo {}".format(end_marker)
1115+
end_markerb = end_marker.encode()
1116+
else:
1117+
end_markerb = prompt
1118+
1119+
# Detect available compression utility, fallback to uncompressed upload.
1120+
compression_mode = None
1121+
possible_compression = ['gzip']
1122+
if six.PY3:
1123+
possible_compression.insert(0, 'xz')
1124+
if not prompt:
1125+
self.sendline("echo {}".format(end_marker).encode())
1126+
if compression == 'auto':
1127+
for utility in possible_compression:
1128+
self.sendlineafter(end_markerb, "command -v {} && echo YEP || echo NOPE;{}".format(utility, echo_end).encode())
1129+
result = self.recvuntil([b'YEP', b'NOPE'])
1130+
if b'YEP' in result:
1131+
compression_mode = utility
1132+
break
1133+
elif compression in possible_compression:
1134+
compression_mode = compression
1135+
else:
1136+
self.error('Invalid compression mode: %s, has to be one of %s', compression, possible_compression)
1137+
1138+
self.debug('Manually uploading using compression mode: %s', compression_mode)
1139+
1140+
if compression_mode == 'xz':
1141+
import lzma
1142+
data = lzma.compress(data, format=lzma.FORMAT_XZ, preset=9)
1143+
compressed_path = target_path + '.xz'
1144+
elif compression_mode == 'gzip':
1145+
import gzip
1146+
data = gzip.compress(data, compresslevel=9)
1147+
compressed_path = target_path + '.gz'
1148+
else:
1149+
compressed_path = target_path
1150+
1151+
# Upload data in `chunk_size` chunks. Assume base64 is available.
1152+
with self.progress('Uploading payload') as p:
1153+
for idx, chunk in enumerate(iters.group(chunk_size, data)):
1154+
if idx == 0:
1155+
self.sendlineafter(end_markerb, "echo {} | base64 -d > {}{}".format(fiddling.b64e(chunk), compressed_path, echo_end).encode())
1156+
else:
1157+
self.sendlineafter(end_markerb, "echo {} | base64 -d >> {}{}".format(fiddling.b64e(chunk), compressed_path, echo_end).encode())
1158+
p.status('{}/{}'.format(idx, len(data)//chunk_size))
1159+
1160+
# Decompress the file and set the permissions.
1161+
if compression_mode is not None:
1162+
self.sendlineafter(end_markerb, '{} -d {}{}'.format(compression_mode, compressed_path, echo_end).encode())
1163+
if chmod_flags:
1164+
self.sendlineafter(end_markerb, 'chmod {} {}{}'.format(chmod_flags, target_path, echo_end).encode())
1165+
if not prompt:
1166+
self.recvuntil(end_markerb)
1167+
10801168
def connect_input(self, other):
10811169
"""connect_input(other)
10821170

0 commit comments

Comments
 (0)