这篇文章主要还是想记录一下 echohub 这道题目,题目很有意思。
Echohub
这道题目比较有意思, Web+Pwn ,用 PHP 写了一个模拟数据进出栈的过程。程序源码经过 enphp 加密,当中有很多字符乱码,而且许多变量名也经过混淆。这里可以直接用 var_export 导出全局变量,或者直接用IDE动态调试。
我们可以看到一个变量置换表。通过编写程序进行自动替换,可生成美化后的代码。替换脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?php include 'index.php';
function replaceString($match){ global $O; $index = -1; if(false !== strpos($match[1], 'x')){ $index = hexdec($match[1]); } else{ $index = $match[1]; } if( function_exists($O[$index]) || class_exists($O[$index]) || defined($O[$index]) ){ return $O[$index]; } else{ return "'".addslashes($O[$index])."'"; }
}
$encrypto_string = file_get_contents('index.php'); $encrypto_string = preg_replace_callback('/\$GLOBALS[\[\{]O0[\]\}][\[\{](\w{1,15})[\]\}]/','replaceString',$encrypto_string); $decrypto_string = preg_replace_callback('/\$[O0]*[\[\{](\w{2,15})[\]\}]/','replaceString',$encrypto_string); file_put_contents('decrypted.php', $decrypto_string); ?>
|
然后手动删掉乱码字符,最后生成的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
| <?php error_reporting(E_ALL^E_NOTICE); define('O0', 'O'); require_once 'sandbox.php';
$seed = time(); srand($seed); define(INS_OFFSET,rand(0x0000,0xffff));
$regs = array( 'eax'=>0x0, 'ebp'=>0x0, 'esp'=>0x0, 'eip'=>0x0, );
function aslr(&$O00,$O0O){ $O00 = $O00 + 0x60000000 + INS_OFFSET + 0x001 ; } $func_ = array_flip($func); array_walk($func_,aslr); $plt = array_flip($func_);
function handle_data($OOO){ $OO0O=&$GLOBALS{O0}; $O000 = strlen($OOO); $O00O = $O000/0x000004+(0x001*($O000%0x000004)); $O0O0 = str_split($OOO,0x000004); $O0O0[$O00O-0x001] = str_pad($O0O0[$O00O-0x001],0x000004,'\0');
foreach ($O0O0 as $O0OO=>&$OO00){ $OO00 = strrev(bin2hex($OO00)); } return $O0O0; }
function gen_canary(){ $O0O00=&$GLOBALS{O0}; $OOOO = 'abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQEST123456789'; $O0000 = $OOOO[rand(0,strlen($OOOO)-0x001)]; $O000O = $OOOO[rand(0,strlen($OOOO)-0x001)]; $O00O0 = $OOOO[rand(0,strlen($OOOO)-0x001)]; $O00OO = '\0'; return handle_data($O0000.$O000O.$O00O0.$O00OO)[0]; } $canary = gen_canary(); $canarycheck = $canary;
function check_canary(){ global $canary; global $canarycheck; if($canary != $canarycheck){ die('emmmmmm...Don\'t attack me!'); } }
Class O0OO0{ private $ebp,$stack,$esp;
public function __construct($O0OOO,$OO000) { $OO00O=&$GLOBALS{O0}; $this->stack = array(); global $regs; $this->ebp = &$regs['ebp']; $this->esp = &$regs['esp']; $this->ebp = 0xfffe0000 + rand(0x0000,0xffff);
global $canary; $this->stack[$this->ebp - 0x4] = &$canary; $this->stack[$this->ebp] = $this->ebp + rand(0x0000,0xffff); $this->esp = $this->ebp - (rand(0x20,0x60)*0x000004); $this->stack[$this->ebp + 0x4] = dechex($O0OOO);
if($OO000 != NULL) $this->{'pushdata'}($OO000); }
public function pushdata($OO0O0){ $OOO00=&$GLOBALS{O0}; $OO0O0 = handle_data($OO0O0); for($OO0OO=0;$OO0OO<count($OO0O0);$OO0OO++){ $this->stack[$this->esp+($OO0OO*0x000004)] = $OO0O0[$OO0OO]; check_canary(); } }
public function recover_data($OOO0O){ $OOOO0=&$GLOBALS{O0}; return hex2bin(strrev($OOO0O)); }
public function outputdata(){ $O0000O=&$GLOBALS{O0}; global $regs;
echo 'root says: '; while(0x001){ if($this->esp == $this->ebp-0x4) break; $this->{'pop'}('eax'); $OOOOO = $this->{'recover_data'}($regs['eax']); $O00000 = explode('\0',$OOOOO); echo $O00000[0];
if(count($O00000)>0x001){ break; } } }
public function ret(){ $O000O0=&$GLOBALS{O0}; $this->esp = $this->ebp; $this->{'pop'}('ebp'); $this->{'pop'}('eip'); $this->{'call'}(); }
public function get_data_from_reg($O000OO){ $O00OO0=&$GLOBALS{O0}; global $regs; $O00O00 = $this->{'recover_data'}($regs[$O000OO]); $O00O0O = explode('\0',$O00O00); return $O00O0O[0]; }
public function call(){ $O0OO00=&$GLOBALS{O0}; global $regs; global $plt; $O00OOO = hexdec($regs['eip']);
if(isset($_REQUEST[$O00OOO])) { $this->{'pop'}('eax'); $O0O000 = (int)$this->{'get_data_from_reg'}('eax'); $O0O00O = array(); for($O0O0O0=0;$O0O0O0<$O0O000;$O0O0O0++){ $this->{'pop'}('eax'); $O0O0OO = $this->{'get_data_from_reg'}('eax'); array_push($O0O00O,$_REQUEST[$O0O0OO]); } call_user_func_array($plt[$O00OOO],$O0O00O); } else { call_user_func($plt[$O00OOO]); } }
public function push($O0OO0O){ $O0OOOO=&$GLOBALS{O0}; global $regs; $O0OOO0 = $regs[$O0OO0O]; if( hex2bin(strrev($O0OOO0)) == NULL ) die('data error'); $this->stack[$this->esp] = $O0OOO0; $this->esp -= 0x000004; }
public function pop($OO0000){ global $regs; $regs[$OO0000] = $this->stack[$this->esp]; $this->esp += 0x000004; }
public function __call($OO000O,$OO00O0){ check_canary(); }
}class_alias(O0OO0,stack,0);print_R(O0OO0);print_R(stack);
if(isset($_POST['data'])) { $phpinfo_addr = array_search(phpinfo, $plt); $gets = $_POST['data']; $main_stack = new stack($phpinfo_addr, $gets); echo '--------------------output---------------------</br></br>'; $main_stack->{'outputdata'}(); echo '</br></br>------------------phpinfo()------------------</br>'; $main_stack->{'ret'}(); }
|
接下来我们来分段看这些函数的功能,代码当中还是有很多 0o 字符变量,我会将它们替换成其他有含义的变量名。
首先程序用 $regs 来模拟 eax、ebp、esp、eip 4个寄存器,然后定义了 aslr 函数模拟地址空间随机化,但是这个随机化的过程存在问题,因为用了可预测的变量做随机数种子,这样地址就会被计算出来。handle_data 函数会对数据进行填充,用 \x00 将其长度填充成4的倍数,然后再以4长度分割。但是这个代码分割方式存在问题,例如我们输入的数据为 123 ,按照正常的逻辑经过处理后,数据应该变成 123\x00
,但是这里的 handle_data 函数却会将其处理成 123\x00\x00\x00\x00\x00
(多了4个 \x00
)。
gen_canary 函数用户生成用于防止栈溢出的 canary 字符串,代码如下:
接着通过 O0OO0 类的构造创建栈帧,其过程如下图右边所示:
接着开始将数据压栈,但是这里的 pushdata 方法貌似写的有问题,先进栈数据的地址应该要比后进栈的数据地址高,即 push 中的写法是正确的,但是却没调用。回到压栈操作本身,可以发现程序并未对数据的长度进行判断,这样有可能会导致数据过长而覆盖原有的数据。虽然有 canary 的保护,但是这个 canary 是可以泄露的,我们完全可以绕过。
完成数据压栈之后,开始进行数据出栈操作。程序会将 canary 到栈顶之间的所有数据都弹出去,之后结束弹栈操作。
完成数据出栈操作后,开始模拟 ret 指令,弹出下一条指令的地址并调用。 eip 上存放的是 phpinfo 函数的地址,但是这个地址可以利用前面 pushdata 可以覆盖成我们想要的函数地址。然后根据 REQUEST 请求参数中是否含有这个函数地址,分别进行有参函数、无参函数的调用。
这个时候,我们就可以执行任意函数了。但是从 phpinfo 的信息可以看出程序设置了 disable_functions :
1
| file_get_contents,file_put_contents,fwrite,file,chmod,chown,copy,link,fflush,mkdir,popen,rename,touch,unlink,pcntl_alarm,move_upload_file,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,fsockopen,pfsockopen,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,curl_init,curl_exec,curl_multi_init,curl_multi_exec,dba_open,dba_popen,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,shell_exec,popen,proc_open,passthru,symlink,link,syslog,imap_open,ld,mail,dl,putenv
|
然而没有禁用 create_function 函数,我们便可以利用该函数实现任意代码执行,具体可以参考:create_function函数如何实现RCE 。但是即便这样,我们也没办法绕过 disable_functions 执行 readflag 程序。
这时,可以把题目给的 dockerfile 拿来跑一下,看下其中开放的服务等等。我们会发现题目环境中开启了默认安装的 php-fpm 服务,这样我们可以通过 unix sock 与 php-fpm 进行通信。
其中,很重要的一点是,当我们使用 unix sock 与 php-fpm 进行通信,应用在PHP的配置文件是 php-fpm 独立的 php.ini 文件,而这个文件中默认 disable_functions 限制就少了很多。我们在题目上看到的 disable_functions ,实际上是 apache 自己的 php.ini 配置。这样,我们就可以通过 unix sock 与 php-fpm 进行通信,从而绕过 disable_functions 限制。
当我们通过 unix sock 与 php-fpm 进行通信,又会发现不管向 index.php、sandbox.php 哪个文件请求,都会触发如下代码:
我们可以通过搜索环境中默认安装 PHP 后存在的 PHP 文件,利用他们作为请求文件即可绕过上面代码。
然而实际上, PHP 中并不允许通过 ini_set 函数来设置 disable_functions 的值,即便你设置了也不会生效。
由于 apache 模式下设置的 disable_functions 没有禁用 stream_socket_client、stream_socket_sendto 这两个函数。我们可以利用其进行 SSRF ,与 PHP-FPM 进行通信,最后按照 fastcig 请求格式构造报文即可。在看官方 exploit.php 时,发现其通过 PHP_VALUE 设置了 disable_functions=空
,而实际上 disable_functions 这个选项是PHP加载的时候就确定了,并不能通过 PHP_VALUE 设置生效。
mywebsql
这道题比较简单,直接利用admin/admin就可以登录mywebsql管理界面。然后利用mywebsql3.7的RCE直接写shell,具体参考: https://github.com/eddietcc/CVEnotes/blob/master/MyWebSQL/RCE/readme.md
之后要执行readflag程序,而这个程序会先给你一串表达式,你要快速的告诉他答案,答对即可获得flag。这里我使用如下PHP程序获得flag:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <?php $descriptorspec = array( 0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => array("file", "/tmp/error-output.txt", "a") );
$process = proc_open('/readflag', $descriptorspec, $pipes, $cwd, $env);
if (is_resource($process)) { $question = fread($pipes[1],1024); $question = fread($pipes[1],1024); $question = trim($question); var_dump($question); eval('$result = '.$question.';'); fwrite($pipes[0], $result); fclose($pipes[0]); var_dump($result);
$flag = fread($pipes[1],1024); $flag = fread($pipes[1],1024); $flag = fread($pipes[1],1024);
fclose($pipes[1]); var_dump($flag); $return_value = proc_close($process);
echo "command returned $return_value\n"; }
?>
|
赛后还看到有选手逆向readflag程序,然后bypass ualarm函数的,具体参考 https://www.zhaoj.in/read-5479.html
参考
官方题解
Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写
echohub by maple
堆栈和函数调用过程
堆栈的工作原理
Canary机制及绕过策略-格式化字符串漏洞泄露Canary
CGI与FastCGI