63

2019*CTF之Web部分题解

 5 years ago
source link: https://mochazz.github.io/2019/05/03/2019星CTF之Web部分题解/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

这道题目比较有意思, Web+Pwn ,用 PHP 写了一个模拟数据进出栈的过程。程序源码经过 enphp 加密,当中有很多字符乱码,而且许多变量名也经过混淆。这里可以直接用 var_export 导出全局变量,或者直接用IDE动态调试。

6FrQF3R.png!web

我们可以看到一个变量置换表。通过编写程序进行自动替换,可生成美化后的代码。替换脚本如下:

<?php
include 'index.php';	// index.php为被加密的文件

function replaceString($match){
    global $O;
    $index = -1;
    if(false !== strpos($match[1], 'x')){
        $index = hexdec($match[1]);
    }
    else{
        $index = $match[1];
    }
    if( function_exists($O[$index]) || class_exists($O[$index]) || defined($O[$index]) ){
        return $O[$index];
    }
    else{
        return "'".addslashes($O[$index])."'";
    }

}

$encrypto_string = file_get_contents('index.php');
$encrypto_string = preg_replace_callback('/\$GLOBALS[\[\{]O0[\]\}][\[\{](\w{1,15})[\]\}]/','replaceString',$encrypto_string);
$decrypto_string = preg_replace_callback('/\$[O0]*[\[\{](\w{2,15})[\]\}]/','replaceString',$encrypto_string);
file_put_contents('decrypted.php', $decrypto_string);
?>

然后手动删掉乱码字符,最后生成的代码如下:

<?php
error_reporting(E_ALL^E_NOTICE);
define('O0', 'O');
require_once 'sandbox.php';

$seed = time();
srand($seed);
define(INS_OFFSET,rand(0x0000,0xffff));

$regs = array(
    'eax'=>0x0,
    'ebp'=>0x0,
    'esp'=>0x0,
    'eip'=>0x0,
);

function aslr(&$O00,$O0O){
    $O00 = $O00 + 0x60000000 + INS_OFFSET + 0x001 ;
}
$func_ = array_flip($func);
array_walk($func_,aslr);
$plt = array_flip($func_);


function handle_data($OOO){
    $OO0O=&$GLOBALS{O0};
    $O000 = strlen($OOO);
    $O00O = $O000/0x000004+(0x001*($O000%0x000004));
    $O0O0 = str_split($OOO,0x000004);
    $O0O0[$O00O-0x001] = str_pad($O0O0[$O00O-0x001],0x000004,'\0');

    foreach ($O0O0  as  $O0OO=>&$OO00){
        $OO00 = strrev(bin2hex($OO00));
    }
    return $O0O0;
}

function gen_canary(){
    $O0O00=&$GLOBALS{O0};
    $OOOO = 'abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQEST123456789';
    $O0000 = $OOOO[rand(0,strlen($OOOO)-0x001)];
    $O000O = $OOOO[rand(0,strlen($OOOO)-0x001)];
    $O00O0 = $OOOO[rand(0,strlen($OOOO)-0x001)];
    $O00OO = '\0';
    return handle_data($O0000.$O000O.$O00O0.$O00OO)[0];
}
$canary = gen_canary();
$canarycheck = $canary;

function check_canary(){
    global $canary;
    global $canarycheck;
    if($canary != $canarycheck){
        die('emmmmmm...Don\'t attack me!');
    }
}

Class O0OO0{
    private  $ebp,$stack,$esp;

    public  function __construct($O0OOO,$OO000) {
        $OO00O=&$GLOBALS{O0};
        $this->stack = array();
        global $regs;
        $this->ebp = &$regs['ebp'];
        $this->esp = &$regs['esp'];
        $this->ebp = 0xfffe0000 + rand(0x0000,0xffff);

        global $canary;
        $this->stack[$this->ebp - 0x4] = &$canary;
        $this->stack[$this->ebp] = $this->ebp + rand(0x0000,0xffff);
        $this->esp = $this->ebp - (rand(0x20,0x60)*0x000004);
        $this->stack[$this->ebp + 0x4] = dechex($O0OOO);

        if($OO000 != NULL)
            $this->{'pushdata'}($OO000);
    }

    public  function pushdata($OO0O0){ 
        $OOO00=&$GLOBALS{O0};
        $OO0O0 = handle_data($OO0O0);
        for($OO0OO=0;$OO0OO<count($OO0O0);$OO0OO++){
            $this->stack[$this->esp+($OO0OO*0x000004)] = $OO0O0[$OO0OO];
            //no args in my stack haha
            check_canary();
        }
    }

    public  function recover_data($OOO0O){
        $OOOO0=&$GLOBALS{O0};
        return hex2bin(strrev($OOO0O));
    }

    public  function outputdata(){
        $O0000O=&$GLOBALS{O0};
        global $regs;

        echo 'root says: ';
        while(0x001){
            if($this->esp == $this->ebp-0x4)
                break;
            $this->{'pop'}('eax');
            $OOOOO = $this->{'recover_data'}($regs['eax']);
            $O00000 = explode('\0',$OOOOO);
            echo $O00000[0];

            if(count($O00000)>0x001){
                break;
            }
        }
    }

    public  function ret(){
        $O000O0=&$GLOBALS{O0};
        $this->esp = $this->ebp;
        $this->{'pop'}('ebp');
        $this->{'pop'}('eip');
        $this->{'call'}();
    }

    public  function get_data_from_reg($O000OO){
        $O00OO0=&$GLOBALS{O0};
        global $regs;
        $O00O00 = $this->{'recover_data'}($regs[$O000OO]);
        $O00O0O = explode('\0',$O00O00);
        return $O00O0O[0];
    }

    public  function call(){
        $O0OO00=&$GLOBALS{O0};
        global $regs;
        global $plt;
        $O00OOO = hexdec($regs['eip']);

        if(isset($_REQUEST[$O00OOO])) {
            $this->{'pop'}('eax');
            $O0O000 = (int)$this->{'get_data_from_reg'}('eax');
            $O0O00O = array();
            for($O0O0O0=0;$O0O0O0<$O0O000;$O0O0O0++){
                $this->{'pop'}('eax');
                $O0O0OO = $this->{'get_data_from_reg'}('eax');
                array_push($O0O00O,$_REQUEST[$O0O0OO]);
            }
            call_user_func_array($plt[$O00OOO],$O0O00O);
        }
        else {
            call_user_func($plt[$O00OOO]);
        }
    }

    public  function push($O0OO0O){
        $O0OOOO=&$GLOBALS{O0};
        global $regs;
        $O0OOO0 = $regs[$O0OO0O];
        if( hex2bin(strrev($O0OOO0)) == NULL ) die('data error');
        $this->stack[$this->esp] = $O0OOO0;
        $this->esp -= 0x000004;
    }

    public  function pop($OO0000){
        global $regs;
        $regs[$OO0000] = $this->stack[$this->esp];
        $this->esp += 0x000004;
    }

    public  function __call($OO000O,$OO00O0){
        check_canary();
    }

}class_alias(O0OO0,stack,0);print_R(O0OO0);print_R(stack);

if(isset($_POST['data'])) {
        $phpinfo_addr = array_search(phpinfo, $plt);
        $gets = $_POST['data'];
        $main_stack = new stack($phpinfo_addr, $gets);
        echo '--------------------output---------------------</br></br>';
        $main_stack->{'outputdata'}();
        echo '</br></br>------------------phpinfo()------------------</br>';
        $main_stack->{'ret'}();
}

接下来我们来分段看这些函数的功能,代码当中还是有很多 0o 字符变量,我会将它们替换成其他有含义的变量名。

首先程序用 $regs 来模拟 eax、ebp、esp、eip 4个寄存器,然后定义了 aslr 函数模拟地址空间随机化,但是这个随机化的过程存在问题,因为用了可预测的变量做随机数种子,这样地址就会被计算出来。 handle_data 函数会对数据进行填充,用 \x00 将其长度填充成4的倍数,然后再以4长度分割。但是这个代码分割方式存在问题,例如我们输入的数据为 123 ,按照正常的逻辑经过处理后,数据应该变成 123\x00 ,但是这里的 handle_data 函数却会将其处理成 123\x00\x00\x00\x00\x00 (多了4个 \x00 )。

YjMZFny.png!web

gen_canary函数用户生成用于防止栈溢出的 canary 字符串,代码如下:

BFZfueM.png!web

接着通过 O0OO0 类的构造创建栈帧,其过程如下图右边所示:

QzYBzmZ.png!web

接着开始将数据压栈,但是这里的 pushdata 方法貌似写的有问题,先进栈数据的地址应该要比后进栈的数据地址高,即 push 中的写法是正确的,但是却没调用。回到压栈操作本身,可以发现程序并未对数据的长度进行判断,这样有可能会导致数据过长而覆盖原有的数据。虽然有 canary 的保护,但是这个 canary 是可以泄露的,我们完全可以绕过。

VjUJBv3.png!web

完成数据压栈之后,开始进行数据出栈操作。程序会将 canary 到栈顶之间的所有数据都弹出去,之后结束弹栈操作。

bMjaeuz.png!web

完成数据出栈操作后,开始模拟 ret 指令,弹出下一条指令的地址并调用。 eip 上存放的是 phpinfo 函数的地址,但是这个地址可以利用前面 pushdata 可以覆盖成我们想要的函数地址。然后根据 REQUEST 请求参数中是否含有这个函数地址,分别进行有参函数、无参函数的调用。

uIVfQnr.png!web

这个时候,我们就可以执行任意函数了。但是从 phpinfo 的信息可以看出程序设置了 disable_functions

file_get_contents,file_put_contents,fwrite,file,chmod,chown,copy,link,fflush,mkdir,popen,rename,touch,unlink,pcntl_alarm,move_upload_file,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,fsockopen,pfsockopen,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,curl_init,curl_exec,curl_multi_init,curl_multi_exec,dba_open,dba_popen,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,dl,putenv

然而没有禁用 create_function 函数,我们便可以利用该函数实现任意代码执行,具体可以参考: create_function函数如何实现RCE 。但是即便这样,我们也没办法绕过 disable_functions 执行 readflag 程序。

这时,可以把题目给的 dockerfile 拿来跑一下,看下其中开放的服务等等。我们会发现题目环境中开启了默认安装的 php-fpm 服务,这样我们可以通过 unix sockphp-fpm 进行通信。

V3QVR3j.png!web

其中,很重要的一点是,当我们使用 unix sockphp-fpm 进行通信,应用在PHP的配置文件是 php-fpm 独立的 php.ini 文件,而这个文件中默认 disable_functions 限制就少了很多。我们在题目上看到的 disable_functions ,实际上是 apache 自己的 php.ini 配置。这样,我们就可以通过 unix sockphp-fpm 进行通信,从而绕过 disable_functions 限制。

NrEreqf.png!web

当我们通过 unix sockphp-fpm 进行通信,又会发现不管向 index.php、sandbox.php 哪个文件请求,都会触发如下代码:

BjauMfi.png!web

我们可以通过搜索环境中默认安装 PHP 后存在的 PHP 文件,利用他们作为请求文件即可绕过上面代码。

uAZnAzM.png!web

然而实际上, PHP 中并不允许通过 ini_set 函数来设置 disable_functions 的值,即便你设置了也不会生效。

fUzqeiV.png!web

由于 apache 模式下设置的 disable_functions 没有禁用 stream_socket_client、stream_socket_sendto 这两个函数。我们可以利用其进行 SSRF ,与 PHP-FPM 进行通信,最后按照 fastcig 请求格式构造报文即可。在看官方 exploit.php 时,发现其通过 PHP_VALUE 设置了 disable_functions=空 ,而实际上 disable_functions 这个选项是PHP加载的时候就确定了,并不能通过 PHP_VALUE 设置生效。

mYBnuyy.png!web

mywebsql

这道题比较简单,直接利用admin/admin就可以登录mywebsql管理界面。然后利用mywebsql3.7的RCE直接写shell,具体参考: https://github.com/eddietcc/CVEnotes/blob/master/MyWebSQL/RCE/readme.md

之后要执行readflag程序,而这个程序会先给你一串表达式,你要快速的告诉他答案,答对即可获得flag。这里我使用如下PHP程序获得flag:

<?php
$descriptorspec = array(
    0 => array("pipe", "r"),  // 标准输入,子进程从此管道中读取数据
    1 => array("pipe", "w"),  // 标准输出,子进程向此管道中写入数据
    2 => array("file", "/tmp/error-output.txt", "a") // 标准错误,写入到一个文件
);

$process = proc_open('/readflag', $descriptorspec, $pipes, $cwd, $env);

if (is_resource($process)) {
    $question = fread($pipes[1],1024); // 获取程序问题
    $question = fread($pipes[1],1024); // 获取程序问题
    $question = trim($question);
    var_dump($question);
    eval('$result = '.$question.';');   // 计算问题结果
    fwrite($pipes[0], $result);         // 回答程序问题
    fclose($pipes[0]);
    var_dump($result);

    // $flag = stream_get_contents($pipes[1]);// getflag
    $flag = fread($pipes[1],1024);
    $flag = fread($pipes[1],1024);
    $flag = fread($pipes[1],1024);

    fclose($pipes[1]);
    var_dump($flag);
    
    $return_value = proc_close($process);

    echo "command returned $return_value\n";
}

?>

赛后还看到有选手逆向readflag程序,然后bypass ualarm函数的,具体参考 https://www.zhaoj.in/read-5479.html

参考


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK