Skip to content

Commit 6f0793e

Browse files
authored
Add tube.upload_manually to upload files in chunks (#2410)
* 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. * Add tests and fix corner cases * Update CHANGELOG * Fix character escaping in tests * Fix gzip compression on Python 2
1 parent 51cbdb4 commit 6f0793e

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ The table below shows which release corresponds to each branch, and what date th
8585
- [#2478][2478] libcdb-cli: add `--offline-only`, refactor unstrip and add fetch parser for download libc-database
8686
- [#2484][2484] Allow to disable caching
8787
- [#2291][2291] Fix attaching to a gdbserver with tuple `gdb.attach(('0.0.0.0',12345))`
88+
- [#2410][2410] Add `tube.upload_manually` to upload files in chunks
8889

8990
[2471]: https://github.com/Gallopsled/pwntools/pull/2471
9091
[2358]: https://github.com/Gallopsled/pwntools/pull/2358
@@ -98,6 +99,7 @@ The table below shows which release corresponds to each branch, and what date th
9899
[2478]: https://github.com/Gallopsled/pwntools/pull/2478
99100
[2484]: https://github.com/Gallopsled/pwntools/pull/2484
100101
[2291]: https://github.com/Gallopsled/pwntools/pull/2291
102+
[2410]: https://github.com/Gallopsled/pwntools/pull/2410
101103

102104
## 4.14.0 (`beta`)
103105

pwnlib/tubes/tube.py

+127
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,131 @@ 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 -f <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
1100+
between ``gzip`` and ``xz`` is chosen with a fallback to uncompressed
1101+
upload.
1102+
1103+
Arguments:
1104+
1105+
data(bytes): The data to upload.
1106+
target_path(str): The path to upload the data to.
1107+
prompt(bytes): The shell prompt to wait for.
1108+
chunk_size(int): The size of each chunk to upload.
1109+
chmod_flags(str): The flags to use with chmod. ``""`` to ignore.
1110+
compression(str): The compression to use. ``auto`` to automatically choose the best compression or ``gzip`` or ``xz``.
1111+
end_marker(str): The marker to use to detect the end of the output. Only used when prompt is not set.
1112+
1113+
Examples:
1114+
1115+
>>> l = listen()
1116+
>>> l.spawn_process('/bin/sh')
1117+
>>> r = remote('127.0.0.1', l.lport)
1118+
>>> r.upload_manually(b'some\\xca\\xfedata\\n', prompt=b'', chmod_flags='')
1119+
>>> r.sendline(b'cat ./payload')
1120+
>>> r.recvline()
1121+
b'some\\xca\\xfedata\\n'
1122+
1123+
>>> r.upload_manually(cyclic(0x1000), target_path='./cyclic_pattern', prompt=b'', chunk_size=0x10, compression='gzip')
1124+
>>> r.sendline(b'sha256sum ./cyclic_pattern')
1125+
>>> r.recvlineS(keepends=False).startswith(sha256sumhex(cyclic(0x1000)))
1126+
True
1127+
1128+
>>> blob = ELF.from_assembly(shellcraft.echo('Hello world!\\n') + shellcraft.exit(0))
1129+
>>> r.upload_manually(blob.data, prompt=b'')
1130+
>>> r.sendline(b'./payload')
1131+
>>> r.recvline()
1132+
b'Hello world!\\n'
1133+
>>> r.close()
1134+
>>> l.close()
1135+
"""
1136+
echo_end = ""
1137+
if not prompt:
1138+
echo_end = "; echo {}".format(end_marker)
1139+
end_markerb = end_marker.encode()
1140+
else:
1141+
end_markerb = prompt
1142+
1143+
# Detect available compression utility, fallback to uncompressed upload.
1144+
compression_mode = None
1145+
possible_compression = ['gzip']
1146+
if six.PY3:
1147+
possible_compression.insert(0, 'xz')
1148+
if not prompt:
1149+
self.sendline("echo {}".format(end_marker).encode())
1150+
if compression == 'auto':
1151+
for utility in possible_compression:
1152+
self.sendlineafter(end_markerb, "command -v {} && echo YEP || echo NOPE{}".format(utility, echo_end).encode())
1153+
result = self.recvuntil([b'YEP', b'NOPE'])
1154+
if b'YEP' in result:
1155+
compression_mode = utility
1156+
break
1157+
elif compression in possible_compression:
1158+
compression_mode = compression
1159+
else:
1160+
self.error('Invalid compression mode: %s, has to be one of %s', compression, possible_compression)
1161+
1162+
self.debug('Manually uploading using compression mode: %s', compression_mode)
1163+
1164+
compressed_data = b''
1165+
if compression_mode == 'xz':
1166+
import lzma
1167+
compressed_data = lzma.compress(data, format=lzma.FORMAT_XZ, preset=9)
1168+
compressed_path = target_path + '.xz'
1169+
elif compression_mode == 'gzip':
1170+
import gzip
1171+
from six import BytesIO
1172+
f = BytesIO()
1173+
with gzip.GzipFile(fileobj=f, mode='wb', compresslevel=9) as g:
1174+
g.write(data)
1175+
compressed_data = f.getvalue()
1176+
compressed_path = target_path + '.gz'
1177+
else:
1178+
compressed_path = target_path
1179+
1180+
# Don't compress if it doesn't reduce the size.
1181+
if len(compressed_data) >= len(data):
1182+
compression_mode = None
1183+
compressed_path = target_path
1184+
else:
1185+
data = compressed_data
1186+
1187+
# Upload data in `chunk_size` chunks. Assume base64 is available.
1188+
with self.progress('Uploading payload') as p:
1189+
for idx, chunk in enumerate(iters.group(chunk_size, data)):
1190+
if None in chunk:
1191+
chunk = chunk[:chunk.index(None)]
1192+
if idx == 0:
1193+
self.sendlineafter(end_markerb, "echo {} | base64 -d > {}{}".format(fiddling.b64e(bytearray(chunk)), compressed_path, echo_end).encode())
1194+
else:
1195+
self.sendlineafter(end_markerb, "echo {} | base64 -d >> {}{}".format(fiddling.b64e(bytearray(chunk)), compressed_path, echo_end).encode())
1196+
p.status('{}/{} {}'.format(idx+1, len(data)//chunk_size+1, misc.size(idx*chunk_size + len(chunk))))
1197+
p.success(misc.size(len(data)))
1198+
1199+
# Decompress the file and set the permissions.
1200+
if compression_mode is not None:
1201+
self.sendlineafter(end_markerb, '{} -d -f {}{}'.format(compression_mode, compressed_path, echo_end).encode())
1202+
if chmod_flags:
1203+
self.sendlineafter(end_markerb, 'chmod {} {}{}'.format(chmod_flags, target_path, echo_end).encode())
1204+
if not prompt:
1205+
self.recvuntil(end_markerb + b'\n')
1206+
10801207
def connect_input(self, other):
10811208
"""connect_input(other)
10821209

0 commit comments

Comments
 (0)