转载

DockerMaze迷宫挑战赛write-up

今年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?

快速检测二进制文件,但是并没有获得更多信息

DockerMaze迷宫挑战赛write-up

接着下载镜像并进行检测

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": {}  }

DockerMaze迷宫挑战赛write-up

看来这儿有个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镜像时出现了错误

DockerMaze迷宫挑战赛write-up

由于这个错误,我们不能打开这个容器。改变策略,写了一个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请求

DockerMaze迷宫挑战赛write-up

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

尝试找到更多信息:

DockerMaze迷宫挑战赛write-up

可以看到容器利用了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直接的通信。

DockerMaze迷宫挑战赛write-up

于是顺手就这么做了:

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"

DockerMaze迷宫挑战赛write-up

ok,方向没错,可惜的是此前我从没参加过Dockercon比赛。幸运的是@nibble_ds将他们给Schibsted展位的key发送给了我。

DockerMaze迷宫挑战赛write-up

扫描二维码出现了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 big.ideas+DockerMaze@schibsted.com: - IP used to escape - The token 'XXXXXXXXXXXXXXXXXX' - Short explanation about how you escaped

完美解决!

如果你想动手玩玩,猛戳 这里

*原文: testpurposes ,编译/FB小编鸢尾,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)

正文到此结束
Loading...