2019*CTF之Web部分题解

这篇文章主要还是想记录一下 echohub 这道题目,题目很有意思。

Echohub

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

3

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

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'; // 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];
//no args in my stack haha
check_canary();
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4

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

5

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

6

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

7

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

8

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

9

这个时候,我们就可以执行任意函数了。但是从 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 sockphp-fpm 进行通信。

10

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

11

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

12

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

13

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

14

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

15

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 = stream_get_contents($pipes[1]);// getflag
$flag = fread($pipes[1],1024);
$flag = fread($pipes[1],1024);
$flag = fread($pipes[1],1024);

fclose($pipes[1]);
var_dump($flag);

$return_value = proc_close($process);

echo "command returned $return_value\n";
}

?>

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

参考

官方题解
Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写
echohub by maple
堆栈和函数调用过程
堆栈的工作原理
Canary机制及绕过策略-格式化字符串漏洞泄露Canary
CGI与FastCGI

文章作者: Mochazz
文章链接: https://mochazz.github.io/2019/05/03/2019星CTF之Web部分题解/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mochazz's blog