今年11月16~17日在巴萨罗那举行的Dockercon eu 2015,Schibsted Team发布了一个DockerMaze挑战赛,就跟我们90年代玩的跑出迷宫游戏差不多。在这个游戏中你在迷宫的中间醒来,你必须逃离迷宫才能生存,通过一个控制台你可以使用命令对环境进行交互操作。
在本文我将介绍下我是怎么脱离苦海的。
从help命令中我们得知有“look”,“interact” 以及 “escape”命令,如果你在游戏中键入“look front”,它会反馈说墙上有标志,之后执行“inspect wall”就会给你一些线索:
Found rooms: - schibstedchallenge/dockermaze-weisse:latest - schibstedchallenge/dockermaze-stout:latest - schibstedchallenge/dockermaze-porter:latest - schibstedchallenge/dockermaze-ipa:latest Found Keys: - FollowTheWhiteRabbit Followed path: - Input: https://challenge.schibsted.com/assets/data/ct1.bin - Output: ? More than a year and I'm still here. I'm loosing all hope. Maybe there is another key?
快速检测二进制文件,但是并没有获得更多信息
接着下载镜像并进行检测
docker pull schibstedchallenge/dockermaze-weisse:latest docker pull schibstedchallenge/dockermaze-stout:latest docker pull schibstedchallenge/dockermaze-porter:latest docker pull schibstedchallenge/dockermaze-ipa:latest
好生玩玩这个docker镜像
WEISSE
docker inspect schibstedchallenge/dockermaze-weisse:latest
相关信息
"Entrypoint": [ "/usr/local/bin/start.bash" ] "ExposedPorts": { "1954/tcp": {} }
看来这儿有个ruby应用(weisse.rb)监听1954/tcp端口,其通过“start.bash”bash脚本执行。在脚本中的额外信息中我们得知:
# We use eureka + prana for service discovery.
这条线索以后会有用
weisse.rb文件暴露了一个REST端点(/turing)运行恩尼格码密码机接收的数据。此外为了设置恩尼格码密码机,其试图获取更多信息进行DNS请求,以下为代码片段:
...SNIP... BFBASE = 'aaa' set :bind, '0.0.0.0'set :port, 1954 post '/turing' do data = request.body.read rotors = get_rotors('porter') plugboard = Hash[*PLUGBOARD.pack('H*').split('')] plugboard.merge!(plugboard.invert) rotors.map! do |r| Hash[[r].pack('H*').split('').zip((0...256).map{|i| i.chr})] end reflector = Hash[*REFLECTOR.pack('H*').split('')] reflector.merge!(reflector.invert) enigma(data, plugboard, rotors, reflector)end ...SNIP... def get_rotors(nameserver) rotors = [] Resolv::DNS.open({:nameserver=>[nameserver]}) do |r| ctr = 0 loop do begin n = r.getresource("walzen-#{ctr}.dockermaze", Resolv::DNS::Resource::IN::TXT).data.to_i rescue Resolv::ResolvError break end bf = BFBASE.dup found_chunks = 0 rotors[ctr] = '' while found_chunks < n begin ck = r.getresource("walzen-#{ctr}-#{bf}.dockermaze", Resolv::DNS::Resource::IN::TXT).data.delete('"') rotors[ctr] << ck found_chunks += 1 rescue Resolv::ResolvError next ensure bf.next! end end ctr += 1 end end rotors end ...SNIP...
STOUT
docker inspect schibstedchallenge/dockermaze-stout
相关信息
"Entrypoint": [ "/usr/local/bin/stout.py" ] "ExposedPorts": { "31337/tcp": {} }
尝试运行docker镜像时出现了错误
由于这个错误,我们不能打开这个容器。改变策略,写了一个Python文件:
docker run -ti --entrypoint /bin/bash --name stout schibstedchallenge/dockermaze-stout docker cp stout:/usr/local/bin/stout.py .
stout.py:
#!/usr/bin/env python import os import sys import socket import base64 from datetime import datetime from dns import resolver from flask import Flask, request, make_response app = Flask('stout') PORTER_HOST = os.getenv('PORTER_PORT_53_TCP_ADDR') def xor(data, key): return "".join(map(lambda i: chr(ord(data[i]) ^ ord(key[i%len(key)])), xrange(len(data)))) def transform(data): s = socket.socket()s.connect(('ipa', 6060)) s.sendall(base64.b64encode(data) + "\n") ret = s.makefile().readline().decode('base64') s.close() return ret @app.route("/gate", methods=['POST'])def gate(): t1 = datetime.now() data = request.stream.read() dns_resolver = resolver.Resolver()dns_resolver.nameservers = [PORTER_HOST] dns_answer = dns_resolver.query('bitwise.dockermaze', 'TXT') secret = dns_answer[0].to_text().strip('"') ret = transform(xor(data, secret)) t2 = datetime.now() resp = make_response(ret, 200) resp.headers.extend({'X-Dockermaze-Time': t2-t1}) return resp if __name__ == '__main__': if not PORTER_HOST: sys.exit('error: cannot get key') app.run(host='0.0.0.0', port=31337)
脚本发布一个REST端点接收密码数据,获得通过DNS请求porter主机(需要通过环境变量提供其IP地址)并将结果发送到ipa主机
PORTER
docker inspect schibstedchallenge/dockermaze-porter
相关信息
"Entrypoint": [ "/usr/sbin/named" ] "ExposedPorts": { "53/tcp": {} }
与stout和weisse容器提供的信息相比,porter更像一个DNS请求
db.dockermaze DNS域名BIND配置中出现的一个条目中包括一个密钥需要stout.py才能工作。
IPA
docker inspect schibstedchallenge/dockermaze-ipa
相关信息
"Entrypoint": [ "/usr/local/bin/start.bash" ] "ExposedPorts": { "6060/tcp": {} } "Env": [ "PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "GOLANG_VERSION=1.5.1", "GOLANG_DOWNLOAD_URL=https://golang.org/dl/go1.5.1.linux-amd64.tar.gz", "GOLANG_DOWNLOAD_SHA1=46eecd290d8803887dec718c691cc243f2175fe0", "GOPATH=/go" ]
golang清晰的指向了@nibble_ds,尝试运行IPA镜像,出现了一个错误:
2015/11/22 12:20:20 error: envvar AES_KEY not defined
尝试找到更多信息:
可以看到容器利用了Prana和Eureka(the Netflix stack的项目)并运行IPA golang二进制。在本例,挑战赛作者为了让我们更轻松还提供了源代码
$ file ipa ipa: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, not stripped
分析IPA应用(ipa.go)的源代码,我们得知其监听6060/tcp端口,并解码(base64)且解密接收到的数据,使用AES-256 CTR模式,环境变量“AES_KEY”提供的key。这个结果发送到“weisse”REST端点并返回Base64编码的数据,然后返回给调用者。片段:
...SNIP... var AesKey = os.Getenv("AES_KEY") func main() { ...SNIP... ln, err := net.Listen("tcp", ":6060") ...SNIP... func handleConnection(conn net.Conn) { defer conn.Close() br := bufio.NewReader(conn) line, err := br.ReadString('\n') ...SNIP... data, err := base64.StdEncoding.DecodeString(line) ...SNIP... decdata, err := decrypt(data, []byte(AesKey)) ...SNIP... transdata, err := transform(decdata) ...SNIP... ret := base64.StdEncoding.EncodeToString([]byte(transdata)) fmt.Fprintln(conn, ret) } ...SNIP... func transform(data []byte) (transdata []byte, err error) { c := goprana.NewClient(goprana.DefaultPort) resp, err := c.Post("weisse", "/turing", "application/octet-stream", bytes.NewReader(data)) if err != nil { return nil, err } defer resp.Body.Close() return ioutil.ReadAll(resp.Body) }
线索整理
1.stout通过POST HTTP方法到其REST端点预计接收一些数据,监听31337/tcp端口。从porter获取密钥(DNS请求),接收的数据转换,发送它的Base64编码到IPA。 2.IPA接收stout发送的数据,解码(Base64)并通过环境变量提供的AES_KEY进行解密,然后将解密后得到的内容发送给weisse端点 3.weisse端点应用一个Enigma解密来接收数据,通过DNS请求porter DNS服务器,然后向IPA反馈解密数据 4.IPA响应bas64编码并返回到stout 5.stout解密bas64响应并提供给调用者
我们有ct1.bin文件以及FollowTheRabbit密钥,大胆做出假设:
AES_KEY就是:FollowTheRabbit ct1.bin就是我们想使用容器链解密的加密数据
为了彼此之间更好的通信,我们需要连接容器,使用Prana & Eureka结合weisse和ipa直接的通信。
于是顺手就这么做了:
docker pull netflixoss/eureka:1.1.147 docker run -d --name eureka netflixoss/eureka:1.1.147 docker run -d -P --name porter schibstedchallenge/dockermaze-porter docker run -d -P --name weisse --link porter:porter --link eureka:eureka schibstedchallenge/dockermaze-weisse docker run -d -P -e "AES_KEY=FollowTheWhiteRabbit" --name ipa --link weisse:weisse --link eureka:eureka schibstedchallenge/dockermaze-ipa docker run -d -p 31337:31337 --name stout --link ipa:ipa --link porter:porter schibstedchallenge/dockermaze-stout
在请求发送到stout端点前我等待了几分钟(遵循start.bash文件中的建议)
curl -v -X POST --data-binary @ct1.bin http://localhost:31337/gate --header "Content-Type:application/octet-stream"
ok,方向没错,可惜的是此前我从没参加过Dockercon比赛。幸运的是@nibble_ds将他们给Schibsted展位的key发送给了我。
扫描二维码出现了ruby代码片段:
puts 'z4LufsdfTf{bNsfldpE'.bytes.map { |ch| (ch.ord - 1).chr }.reverse.join
执行之后,你可以获得一个新的key(DockerMazeSecretK3y)。当我再次尝试curl命令修改AES_KEY,ct1.bin文件并不响应我们,我们该怎么获得新的消息呢?犹记得DockerMaze的escape命令接收一个ip参数,所以我在我的公共IP,31337/tcp端口上做了一个DNAT映射到stout(PublicIP:31337 -> PrivateIP:31337),并执行:
escape x.x.x.x
x.x.x.x是我的公共IP
Trying to escape... Wait... Hummm… Everything seems to be okay but you must be faster… 20.040787 seconds is too much
为了更高效,我们发现weisse实在太拖节奏了。问题就在这个循环:
...SNIP...BFBASE = 'aaa'...SNIP... def get_rotors(nameserver) rotors = [] Resolv::DNS.open({:nameserver=>[nameserver]}) do |r| ctr = 0 loop do begin n = r.getresource("walzen-#{ctr}.dockermaze", Resolv::DNS::Resource::IN::TXT).data.to_i rescue Resolv::ResolvError break end bf = BFBASE.dup found_chunks = 0 rotors[ctr] = '' while found_chunks < n begin ck = r.getresource("walzen-#{ctr}-#{bf}.dockermaze", Resolv::DNS::Resource::IN::TXT).data.delete('"') rotors[ctr] << ck found_chunks += 1 rescue Resolv::ResolvError next ensure bf.next! end end ctr += 1 end end rotors end
大多数的请求DNS条目就像这样:
walzen-0 IN TXT "4" walzen-0-aaa IN TXT "7b57e0a216b65a40534e4c8bcc787a8e5b3722657dcfb0d199950688ef0c718cbf1094bd0ff7d687c69cfba09d42caaa13d4cdb24f8f892877b4a91f596b2615" walzen-0-aab IN TXT "6f48936c561d66625e31702143c2978ddaf19f60dcfd340e3b3c2b725404a820613ad369ae0a30a5b76de14d08d041337c02ceacbed5e7c3deee67ad7f63f529" walzen-0-aac IN TXT "f3523e2746b1e524a48451ff1e5c92f6d796b9b89036c43d8ae8f486c7c1bc2ea601499e6eab81e383c0392c2d0514f0e9324af985507efa116a743523cb00fe" walzen-0-aad IN TXT "1a68df6455c8ec914476fcc5808279f298ed3f5dbba7a3b54b250309d92f17a112b307db75eb1c2af8dd38e473d819afd2e2ea1be6c90b589aba5f470d18459b" walzen-1 IN TXT "4" walzen-1-aaa IN TXT "0ec8580062742e72c3d96fc76d4f21bacdf03887256bb7c9d42a27c5cb43e216405163e7a3427a071033ea3944899f88d63f83e41d91ad1a19b39c455c041294" walzen-1-aab IN TXT "ac8ccfafe184de033afdf13ddcdfd27b8b86989e82d1ffbca99290fbc4a115c2eba05323be80060a30eeaed3689385148aa56e37a6bb4a1bef0db5bd34dbf846" walzen-1-aac IN TXT "eddd05480c7df9c6d0b69b591eb48f7f20175022f4577170ab7ea78e77b04c5d4e029dbf47fa3e8d49e3d83b4b816999ecb178f561081c292f2b6097544136f7" walzen-1-aad IN TXT "18a46635e9b9f6d756753cf35f65e0aac1266c7c5ba25231e5e60bce0f2cb82432da675a09132d5e9acafc76a8110155b2c04d1ff2d5fe73
但是有一些并不连续,导致了许多不必要的DNS请求错误:
walzen-9 IN TXT "4" walzen-9-aaa IN TXT "3a8a13373496029d73b8d44e23147e947f45d5fd8640073f2ff7953858bb5ce076cfbef68860d8986a7a8fc8ad26d9d3f8fc9fee0e56ed65b14cb0fe84acc724" walzen-9-aab IN TXT "299cda0f3001505a3caf0b99e2c380f3b532161aa861b2f00675dfa4d08da0ea550dcc53f581692a5bd6d119744272fac0b7db8c6210ffbc8bc9a166bacd9305" walzen-9-aac IN TXT "a76f638943aae11d925d680948e4672dd252ab54495fc2caf27cf45133b65e7d6ba6820a1225398e214b274dbd00596efbefe6229e18473b20c56de3c135153d" walzen-9-rzd IN TXT "644fe5f18583ebf9c62e1e1f7bc4ecdd44b4ce70a5086c4ad7579a17a9413e3171bfde11790c877704a2a3e8b32891369bb946e9ae2b2c1b
接下来编辑db.dockermaze配置文件,让他们全部保持连续性并更新docker镜像:
docker cp ./modified-db.dockermaze porter:/etc/bind/db.dockermaze docker commit porter redsadic/dockermaze-porter:v2 docker run -d -P --name porter redsadic/dockermaze-porter:v2
再次运行escape x.x.x.x命令:
Trying to escape... Wait... You put the key in the lock and... the door opens! Congratulations! You are out of the labyrinth! Send an email with the following info to [email protected]: - IP used to escape - The token 'XXXXXXXXXXXXXXXXXX' - Short explanation about how you escaped
完美解决!
如果你想动手玩玩,猛戳这里
*原文:testpurposes,编译/ 鸢尾,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)