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

源链接

Hacking more

...